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