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