38f58af3814fcc484f966112af59ec4f96618129
[fanfix.git] / src / be / nikiroo / fanfix / Main.java
1 package be.nikiroo.fanfix;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.MalformedURLException;
6 import java.net.URL;
7
8 import be.nikiroo.fanfix.bundles.StringId;
9 import be.nikiroo.fanfix.data.Chapter;
10 import be.nikiroo.fanfix.data.Story;
11 import be.nikiroo.fanfix.output.BasicOutput;
12 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
13 import be.nikiroo.fanfix.reader.BasicReader;
14 import be.nikiroo.fanfix.reader.BasicReader.ReaderType;
15 import be.nikiroo.fanfix.supported.BasicSupport;
16 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
17 import be.nikiroo.utils.Progress;
18 import be.nikiroo.utils.Version;
19 import be.nikiroo.utils.serial.Server;
20
21 /**
22 * Main program entry point.
23 *
24 * @author niki
25 */
26 public class Main {
27 private enum MainAction {
28 IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION, SERVER, REMOTE,
29 }
30
31 /**
32 * Main program entry point.
33 * <p>
34 * Known environment variables:
35 * <ul>
36 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
37 * {@link String}s when possible</li>
38 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
39 * before taking the usual ones; they will also be saved/updated into this
40 * path when the program starts</li>
41 * <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
42 * configuration value with 'true'</li>
43 * </ul>
44 * <p>
45 * <ul>
46 * <li>--import [URL]: import into library</li>
47 * <li>--export [id] [output_type] [target]: export story to target</li>
48 * <li>--convert [URL] [output_type] [target] (+info): convert URL into
49 * target</li>
50 * <li>--read [id] ([chapter number]): read the given story from the library
51 * </li>
52 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
53 * story, without saving it</li>
54 * <li>--list ([type]): list the stories present in the library</li>
55 * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
56 * for this command</li>
57 * <li>--version: get the version of the program</li>
58 * <li>--server [port]: start a server on this port</li>
59 * <li>--remote [host] [port]: use a the given remote library</li>
60 * </ul>
61 *
62 * @param args
63 * see method description
64 */
65 public static void main(String[] args) {
66 String urlString = null;
67 String luid = null;
68 String sourceString = null;
69 String chapString = null;
70 String target = null;
71 MainAction action = MainAction.START;
72 Boolean plusInfo = null;
73 String host = null;
74 Integer port = null;
75
76 boolean noMoreActions = false;
77
78 int exitCode = 0;
79 for (int i = 0; exitCode == 0 && i < args.length; i++) {
80 // Action (--) handling:
81 if (!noMoreActions && args[i].startsWith("--")) {
82 if (args[i].equals("--")) {
83 noMoreActions = true;
84 } else {
85 try {
86 action = MainAction.valueOf(args[i].substring(2)
87 .toUpperCase().replace("-", "_"));
88 } catch (Exception e) {
89 Instance.syserr(new IllegalArgumentException(
90 "Unknown action: " + args[i], e));
91 exitCode = 255;
92 }
93 }
94
95 continue;
96 }
97
98 switch (action) {
99 case IMPORT:
100 if (urlString == null) {
101 urlString = args[i];
102 } else {
103 exitCode = 255;
104 }
105 break;
106 case EXPORT:
107 if (luid == null) {
108 luid = args[i];
109 } else if (sourceString == null) {
110 sourceString = args[i];
111 } else if (target == null) {
112 target = args[i];
113 } else {
114 exitCode = 255;
115 }
116 break;
117 case CONVERT:
118 if (urlString == null) {
119 urlString = args[i];
120 } else if (sourceString == null) {
121 sourceString = args[i];
122 } else if (target == null) {
123 target = args[i];
124 } else if (plusInfo == null) {
125 if ("+info".equals(args[i])) {
126 plusInfo = true;
127 } else {
128 exitCode = 255;
129 }
130 } else {
131 exitCode = 255;
132 }
133 break;
134 case LIST:
135 if (sourceString == null) {
136 sourceString = args[i];
137 } else {
138 exitCode = 255;
139 }
140 break;
141 case READ:
142 if (luid == null) {
143 luid = args[i];
144 } else if (chapString == null) {
145 chapString = args[i];
146 } else {
147 exitCode = 255;
148 }
149 break;
150 case READ_URL:
151 if (urlString == null) {
152 urlString = args[i];
153 } else if (chapString == null) {
154 chapString = args[i];
155 } else {
156 exitCode = 255;
157 }
158 break;
159 case HELP:
160 exitCode = 255;
161 break;
162 case SET_READER:
163 exitCode = setReaderType(args[i]);
164 action = MainAction.START;
165 break;
166 case START:
167 exitCode = 255; // not supposed to be selected by user
168 break;
169 case VERSION:
170 exitCode = 255; // no arguments for this option
171 break;
172 case SERVER:
173 if (port == null) {
174 port = Integer.parseInt(args[i]);
175 } else {
176 exitCode = 255;
177 }
178 break;
179 case REMOTE:
180 if (host == null) {
181 host = args[i];
182 } else if (port == null) {
183 port = Integer.parseInt(args[i]);
184 try {
185 BasicReader.setDefaultLibrary(new RemoteLibrary(host,
186 port));
187 } catch (IOException e) {
188 Instance.syserr(e);
189 }
190 action = MainAction.START;
191 } else {
192 exitCode = 255;
193 }
194 break;
195 }
196 }
197
198 final Progress mainProgress = new Progress(0, 80);
199 mainProgress.addProgressListener(new Progress.ProgressListener() {
200 private int current = mainProgress.getMin();
201
202 public void progress(Progress progress, String name) {
203 int diff = progress.getProgress() - current;
204 current += diff;
205
206 StringBuilder builder = new StringBuilder();
207 for (int i = 0; i < diff; i++) {
208 builder.append('.');
209 }
210
211 System.err.print(builder.toString());
212
213 if (progress.isDone()) {
214 System.err.println("");
215 }
216 }
217 });
218 Progress pg = new Progress();
219 mainProgress.addProgress(pg, mainProgress.getMax());
220
221 VersionCheck updates = VersionCheck.check();
222 if (updates.isNewVersionAvailable()) {
223 // Sent to syserr so not to cause problem if one tries to capture a
224 // story content in text mode
225 System.err
226 .println("A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
227 System.err.println("");
228 for (Version v : updates.getNewer()) {
229 System.err.println("\tVersion " + v);
230 System.err.println("\t-------------");
231 System.err.println("");
232 for (String item : updates.getChanges().get(v)) {
233 System.err.println("\t- " + item);
234 }
235 System.err.println("");
236 }
237 }
238
239 if (exitCode != 255) {
240 switch (action) {
241 case IMPORT:
242 exitCode = imprt(urlString, pg);
243 updates.ok(); // we consider it read
244 break;
245 case EXPORT:
246 exitCode = export(luid, sourceString, target, pg);
247 updates.ok(); // we consider it read
248 break;
249 case CONVERT:
250 exitCode = convert(urlString, sourceString, target,
251 plusInfo == null ? false : plusInfo, pg);
252 updates.ok(); // we consider it read
253 break;
254 case LIST:
255 exitCode = list(sourceString);
256 break;
257 case READ:
258 exitCode = read(luid, chapString, true);
259 break;
260 case READ_URL:
261 exitCode = read(urlString, chapString, false);
262 break;
263 case HELP:
264 syntax(true);
265 exitCode = 0;
266 break;
267 case SET_READER:
268 exitCode = 255;
269 break;
270 case VERSION:
271 System.out
272 .println(String.format("Fanfix version %s"
273 + "\nhttps://github.com/nikiroo/fanfix/"
274 + "\n\tWritten by Nikiroo",
275 Version.getCurrentVersion()));
276 updates.ok(); // we consider it read
277 break;
278 case START:
279 BasicReader.getReader().browse(null);
280 break;
281 case SERVER:
282 if (port == null) {
283 exitCode = 255;
284 break;
285 }
286 try {
287 Server server = new RemoteLibraryServer(port);
288 server.start();
289 System.out.println("Remote server started on: " + port);
290 } catch (IOException e) {
291 Instance.syserr(e);
292 }
293 return;
294 case REMOTE:
295 exitCode = 255;
296 break;
297 }
298 }
299
300 if (exitCode == 255) {
301 syntax(false);
302 }
303
304 if (exitCode != 0) {
305 System.exit(exitCode);
306 }
307 }
308
309 /**
310 * Import the given resource into the {@link Library}.
311 *
312 * @param urlString
313 * the resource to import
314 * @param pg
315 * the optional progress reporter
316 *
317 * @return the exit return code (0 = success)
318 */
319 public static int imprt(String urlString, Progress pg) {
320 try {
321 Story story = Instance.getLibrary().imprt(
322 BasicReader.getUrl(urlString), pg);
323 System.out.println(story.getMeta().getLuid() + ": \""
324 + story.getMeta().getTitle() + "\" imported.");
325 } catch (IOException e) {
326 Instance.syserr(e);
327 return 1;
328 }
329
330 return 0;
331 }
332
333 /**
334 * Export the {@link Story} from the {@link Library} to the given target.
335 *
336 * @param luid
337 * the story LUID
338 * @param typeString
339 * the {@link OutputType} to use
340 * @param target
341 * the target
342 * @param pg
343 * the optional progress reporter
344 *
345 * @return the exit return code (0 = success)
346 */
347 public static int export(String luid, String typeString, String target,
348 Progress pg) {
349 OutputType type = OutputType.valueOfNullOkUC(typeString);
350 if (type == null) {
351 Instance.syserr(new Exception(trans(StringId.OUTPUT_DESC,
352 typeString)));
353 return 1;
354 }
355
356 try {
357 Instance.getLibrary().export(luid, type, target, pg);
358 } catch (IOException e) {
359 Instance.syserr(e);
360 return 4;
361 }
362
363 return 0;
364 }
365
366 /**
367 * List the stories of the given source from the {@link Library} (unless
368 * NULL is passed, in which case all stories will be listed).
369 *
370 * @param source
371 * the source to list the known stories of, or NULL to list all
372 * stories
373 *
374 * @return the exit return code (0 = success)
375 */
376 private static int list(String source) {
377 BasicReader.getReader().browse(source);
378 return 0;
379 }
380
381 /**
382 * Start the CLI reader for this {@link Story}.
383 *
384 * @param story
385 * the LUID of the {@link Story} in the {@link Library} <b>or</b>
386 * the {@link Story} {@link URL}
387 * @param chapString
388 * which {@link Chapter} to read (starting at 1), or NULL to get
389 * the {@link Story} description
390 * @param library
391 * TRUE if the source is the {@link Story} LUID, FALSE if it is a
392 * {@link URL}
393 *
394 * @return the exit return code (0 = success)
395 */
396 private static int read(String story, String chapString, boolean library) {
397 try {
398 BasicReader reader = BasicReader.getReader();
399 if (library) {
400 reader.setStory(story, null);
401 } else {
402 reader.setStory(BasicReader.getUrl(story), null);
403 }
404
405 if (chapString != null) {
406 try {
407 reader.read(Integer.parseInt(chapString));
408 } catch (NumberFormatException e) {
409 Instance.syserr(new IOException(
410 "Chapter number cannot be parsed: " + chapString, e));
411 return 2;
412 }
413 } else {
414 reader.read();
415 }
416 } catch (IOException e) {
417 Instance.syserr(e);
418 return 1;
419 }
420
421 return 0;
422 }
423
424 /**
425 * Convert the {@link Story} into another format.
426 *
427 * @param urlString
428 * the source {@link Story} to convert
429 * @param typeString
430 * the {@link OutputType} to convert to
431 * @param target
432 * the target file
433 * @param infoCover
434 * TRUE to also export the cover and info file, even if the given
435 * {@link OutputType} does not usually save them
436 * @param pg
437 * the optional progress reporter
438 *
439 * @return the exit return code (0 = success)
440 */
441 private static int convert(String urlString, String typeString,
442 String target, boolean infoCover, Progress pg) {
443 int exitCode = 0;
444
445 String sourceName = urlString;
446 try {
447 URL source = BasicReader.getUrl(urlString);
448 sourceName = source.toString();
449 if (source.toString().startsWith("file://")) {
450 sourceName = sourceName.substring("file://".length());
451 }
452
453 OutputType type = OutputType.valueOfAllOkUC(typeString);
454 if (type == null) {
455 Instance.syserr(new IOException(trans(
456 StringId.ERR_BAD_OUTPUT_TYPE, typeString)));
457
458 exitCode = 2;
459 } else {
460 try {
461 BasicSupport support = BasicSupport.getSupport(source);
462
463 if (support != null) {
464 Progress pgIn = new Progress();
465 Progress pgOut = new Progress();
466 if (pg != null) {
467 pg.setMax(2);
468 pg.addProgress(pgIn, 1);
469 pg.addProgress(pgOut, 1);
470 }
471
472 Story story = support.process(source, pgIn);
473 try {
474 target = new File(target).getAbsolutePath();
475 BasicOutput.getOutput(type, infoCover).process(
476 story, target, pgOut);
477 } catch (IOException e) {
478 Instance.syserr(new IOException(trans(
479 StringId.ERR_SAVING, target), e));
480 exitCode = 5;
481 }
482 } else {
483 Instance.syserr(new IOException(trans(
484 StringId.ERR_NOT_SUPPORTED, source)));
485
486 exitCode = 4;
487 }
488 } catch (IOException e) {
489 Instance.syserr(new IOException(trans(StringId.ERR_LOADING,
490 sourceName), e));
491 exitCode = 3;
492 }
493 }
494 } catch (MalformedURLException e) {
495 Instance.syserr(new IOException(trans(StringId.ERR_BAD_URL,
496 sourceName), e));
497 exitCode = 1;
498 }
499
500 return exitCode;
501 }
502
503 /**
504 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
505 *
506 * @param id
507 * the ID to translate
508 *
509 * @return the translated result
510 */
511 private static String trans(StringId id, Object... params) {
512 return Instance.getTrans().getString(id, params);
513 }
514
515 /**
516 * Display the correct syntax of the program to the user to stdout, or an
517 * error message if the syntax used was wrong on stderr.
518 *
519 * @param showHelp
520 * TRUE to show the syntax help, FALSE to show "syntax error"
521 */
522 private static void syntax(boolean showHelp) {
523 if (showHelp) {
524 StringBuilder builder = new StringBuilder();
525 for (SupportType type : SupportType.values()) {
526 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
527 type.getDesc()));
528 builder.append('\n');
529 }
530
531 String typesIn = builder.toString();
532 builder.setLength(0);
533
534 for (OutputType type : OutputType.values()) {
535 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
536 type.getDesc(true)));
537 builder.append('\n');
538 }
539
540 String typesOut = builder.toString();
541
542 System.out.println(trans(StringId.HELP_SYNTAX, typesIn, typesOut));
543 } else {
544 System.err.println(trans(StringId.ERR_SYNTAX));
545 }
546 }
547
548 /**
549 * Set the default reader type for this session only (it can be changed in
550 * the configuration file, too, but this value will override it).
551 *
552 * @param readerTypeString
553 * the type
554 */
555 private static int setReaderType(String readerTypeString) {
556 try {
557 ReaderType readerType = ReaderType.valueOf(readerTypeString
558 .toUpperCase());
559 BasicReader.setDefaultReaderType(readerType);
560 return 0;
561 } catch (IllegalArgumentException e) {
562 Instance.syserr(new IOException("Unknown reader type: "
563 + readerTypeString, e));
564 return 1;
565 }
566 }
567 }