Update nikiroo-utils, remove Instance.syserr/trace
[nikiroo-utils.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.BasicSupport.SupportType;
25 import be.nikiroo.utils.Progress;
26 import be.nikiroo.utils.TraceHandler;
27 import be.nikiroo.utils.Version;
28 import be.nikiroo.utils.serial.server.ConnectActionClientObject;
29 import be.nikiroo.utils.serial.server.ServerObject;
30
31 /**
32 * Main program entry point.
33 *
34 * @author niki
35 */
36 public class Main {
37 private enum MainAction {
38 IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION, SERVER, STOP_SERVER, REMOTE,
39 }
40
41 /**
42 * Main program entry point.
43 * <p>
44 * Known environment variables:
45 * <ul>
46 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
47 * {@link String}s when possible</li>
48 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
49 * before taking the usual ones; they will also be saved/updated into this
50 * path when the program starts</li>
51 * <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
52 * configuration value with 'true'</li>
53 * </ul>
54 * <p>
55 * <ul>
56 * <li>--import [URL]: import into library</li>
57 * <li>--export [id] [output_type] [target]: export story to target</li>
58 * <li>--convert [URL] [output_type] [target] (+info): convert URL into
59 * target</li>
60 * <li>--read [id] ([chapter number]): read the given story from the library
61 * </li>
62 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
63 * story, without saving it</li>
64 * <li>--list ([type]): list the stories present in the library</li>
65 * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
66 * for this command</li>
67 * <li>--version: get the version of the program</li>
68 * <li>--server [key] [port]: start a server on this port</li>
69 * <li>--stop-server [key] [port]: stop the running server on this port if
70 * any</li>
71 * <li>--remote [key] [host] [port]: use a the given remote library</li>
72 * </ul>
73 *
74 * @param args
75 * see method description
76 */
77 public static void main(String[] args) {
78 String urlString = null;
79 String luid = null;
80 String sourceString = null;
81 String chapString = null;
82 String target = null;
83 String key = null;
84 MainAction action = MainAction.START;
85 Boolean plusInfo = null;
86 String host = null;
87 Integer port = null;
88
89 boolean noMoreActions = false;
90
91 int exitCode = 0;
92 for (int i = 0; exitCode == 0 && i < args.length; i++) {
93 // Action (--) handling:
94 if (!noMoreActions && args[i].startsWith("--")) {
95 if (args[i].equals("--")) {
96 noMoreActions = true;
97 } else {
98 try {
99 action = MainAction.valueOf(args[i].substring(2)
100 .toUpperCase().replace("-", "_"));
101 } catch (Exception e) {
102 Instance.getTraceHandler().error(
103 new IllegalArgumentException("Unknown action: "
104 + args[i], e));
105 exitCode = 255;
106 }
107 }
108
109 continue;
110 }
111
112 switch (action) {
113 case IMPORT:
114 if (urlString == null) {
115 urlString = args[i];
116 } else {
117 exitCode = 255;
118 }
119 break;
120 case EXPORT:
121 if (luid == null) {
122 luid = args[i];
123 } else if (sourceString == null) {
124 sourceString = args[i];
125 } else if (target == null) {
126 target = args[i];
127 } else {
128 exitCode = 255;
129 }
130 break;
131 case CONVERT:
132 if (urlString == null) {
133 urlString = args[i];
134 } else if (sourceString == null) {
135 sourceString = args[i];
136 } else if (target == null) {
137 target = args[i];
138 } else if (plusInfo == null) {
139 if ("+info".equals(args[i])) {
140 plusInfo = true;
141 } else {
142 exitCode = 255;
143 }
144 } else {
145 exitCode = 255;
146 }
147 break;
148 case LIST:
149 if (sourceString == null) {
150 sourceString = args[i];
151 } else {
152 exitCode = 255;
153 }
154 break;
155 case READ:
156 if (luid == null) {
157 luid = args[i];
158 } else if (chapString == null) {
159 chapString = args[i];
160 } else {
161 exitCode = 255;
162 }
163 break;
164 case READ_URL:
165 if (urlString == null) {
166 urlString = args[i];
167 } else if (chapString == null) {
168 chapString = args[i];
169 } else {
170 exitCode = 255;
171 }
172 break;
173 case HELP:
174 exitCode = 255;
175 break;
176 case SET_READER:
177 exitCode = setReaderType(args[i]);
178 action = MainAction.START;
179 break;
180 case START:
181 exitCode = 255; // not supposed to be selected by user
182 break;
183 case VERSION:
184 exitCode = 255; // no arguments for this option
185 break;
186 case SERVER:
187 case STOP_SERVER:
188 if (key == null) {
189 key = args[i];
190 } else if (port == null) {
191 port = Integer.parseInt(args[i]);
192 } else {
193 exitCode = 255;
194 }
195 break;
196 case REMOTE:
197 if (key == null) {
198 key = args[i];
199 } else if (host == null) {
200 host = args[i];
201 } else if (port == null) {
202 port = Integer.parseInt(args[i]);
203
204 File remoteCacheDir = Instance.getRemoteDir(host);
205 BasicLibrary lib = new RemoteLibrary(key, host, port);
206 lib = new CacheLibrary(remoteCacheDir, lib);
207
208 BasicReader.setDefaultLibrary(lib);
209
210 action = MainAction.START;
211 } else {
212 exitCode = 255;
213 }
214 break;
215 }
216 }
217
218 final Progress mainProgress = new Progress(0, 80);
219 mainProgress.addProgressListener(new Progress.ProgressListener() {
220 private int current = mainProgress.getMin();
221
222 @Override
223 public void progress(Progress progress, String name) {
224 int diff = progress.getProgress() - current;
225 current += diff;
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(new TraceHandler(true, true, true));
338 server.start();
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 try {
350 final String fkey = key;
351 new ConnectActionClientObject(host, port, true) {
352 @Override
353 public void action(Version serverVersion)
354 throws Exception {
355 try {
356 send(new Object[] { fkey, "EXIT" });
357 } catch (Exception e) {
358 Instance.getTraceHandler().error(e);
359 }
360 }
361 }.connect();
362 } catch (IOException e) {
363 Instance.getTraceHandler().error(e);
364 }
365 break;
366 case REMOTE:
367 exitCode = 255; // should not be reachable (REMOTE -> START)
368 break;
369 }
370 }
371
372 if (exitCode == 255) {
373 syntax(false);
374 }
375
376 if (exitCode != 0) {
377 System.exit(exitCode);
378 }
379 }
380
381 /**
382 * Import the given resource into the {@link LocalLibrary}.
383 *
384 * @param urlString
385 * the resource to import
386 * @param pg
387 * the optional progress reporter
388 *
389 * @return the exit return code (0 = success)
390 */
391 public static int imprt(String urlString, Progress pg) {
392 try {
393 Story story = Instance.getLibrary().imprt(
394 BasicReader.getUrl(urlString), pg);
395 System.out.println(story.getMeta().getLuid() + ": \""
396 + story.getMeta().getTitle() + "\" imported.");
397 } catch (IOException e) {
398 Instance.getTraceHandler().error(e);
399 return 1;
400 }
401
402 return 0;
403 }
404
405 /**
406 * Export the {@link Story} from the {@link LocalLibrary} to the given
407 * target.
408 *
409 * @param luid
410 * the story LUID
411 * @param typeString
412 * the {@link OutputType} to use
413 * @param target
414 * the target
415 * @param pg
416 * the optional progress reporter
417 *
418 * @return the exit return code (0 = success)
419 */
420 public static int export(String luid, String typeString, String target,
421 Progress pg) {
422 OutputType type = OutputType.valueOfNullOkUC(typeString, null);
423 if (type == null) {
424 Instance.getTraceHandler().error(
425 new Exception(trans(StringId.OUTPUT_DESC, typeString)));
426 return 1;
427 }
428
429 try {
430 Instance.getLibrary().export(luid, type, target, pg);
431 } catch (IOException e) {
432 Instance.getTraceHandler().error(e);
433 return 4;
434 }
435
436 return 0;
437 }
438
439 /**
440 * List the stories of the given source from the {@link LocalLibrary}
441 * (unless NULL is passed, in which case all stories will be listed).
442 *
443 * @param source
444 * the source to list the known stories of, or NULL to list all
445 * stories
446 *
447 * @return the exit return code (0 = success)
448 */
449 private static int list(String source) {
450 List<MetaData> stories;
451 stories = BasicReader.getReader().getLibrary().getListBySource(source);
452
453 for (MetaData story : stories) {
454 String author = "";
455 if (story.getAuthor() != null && !story.getAuthor().isEmpty()) {
456 author = " (" + story.getAuthor() + ")";
457 }
458
459 System.out.println(story.getLuid() + ": " + story.getTitle()
460 + author);
461 }
462 return 0;
463 }
464
465 /**
466 * Start the CLI reader for this {@link Story}.
467 *
468 * @param story
469 * the LUID of the {@link Story} in the {@link LocalLibrary}
470 * <b>or</b> the {@link Story} {@link URL}
471 * @param chapString
472 * which {@link Chapter} to read (starting at 1), or NULL to get
473 * the {@link Story} description
474 * @param library
475 * TRUE if the source is the {@link Story} LUID, FALSE if it is a
476 * {@link URL}
477 *
478 * @return the exit return code (0 = success)
479 */
480 private static int read(String story, String chapString, boolean library) {
481 try {
482 Reader reader = BasicReader.getReader();
483 if (library) {
484 reader.setMeta(story);
485 } else {
486 reader.setMeta(BasicReader.getUrl(story), null);
487 }
488
489 if (chapString != null) {
490 try {
491 reader.setChapter(Integer.parseInt(chapString));
492 reader.read();
493 } catch (NumberFormatException e) {
494 Instance.getTraceHandler().error(
495 new IOException("Chapter number cannot be parsed: "
496 + chapString, e));
497 return 2;
498 }
499 } else {
500 reader.read();
501 }
502 } catch (IOException e) {
503 Instance.getTraceHandler().error(e);
504 return 1;
505 }
506
507 return 0;
508 }
509
510 /**
511 * Convert the {@link Story} into another format.
512 *
513 * @param urlString
514 * the source {@link Story} to convert
515 * @param typeString
516 * the {@link OutputType} to convert to
517 * @param target
518 * the target file
519 * @param infoCover
520 * TRUE to also export the cover and info file, even if the given
521 * {@link OutputType} does not usually save them
522 * @param pg
523 * the optional progress reporter
524 *
525 * @return the exit return code (0 = success)
526 */
527 private static int convert(String urlString, String typeString,
528 String target, boolean infoCover, Progress pg) {
529 int exitCode = 0;
530
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 Progress pgIn = new Progress();
552 Progress pgOut = new Progress();
553 if (pg != null) {
554 pg.setMax(2);
555 pg.addProgress(pgIn, 1);
556 pg.addProgress(pgOut, 1);
557 }
558
559 Story story = support.process(source, pgIn);
560 try {
561 target = new File(target).getAbsolutePath();
562 BasicOutput.getOutput(type, infoCover).process(
563 story, target, pgOut);
564 } catch (IOException e) {
565 Instance.getTraceHandler().error(
566 new IOException(trans(StringId.ERR_SAVING,
567 target), e));
568 exitCode = 5;
569 }
570 } else {
571 Instance.getTraceHandler().error(
572 new IOException(trans(
573 StringId.ERR_NOT_SUPPORTED, source)));
574
575 exitCode = 4;
576 }
577 } catch (IOException e) {
578 Instance.getTraceHandler().error(
579 new IOException(trans(StringId.ERR_LOADING,
580 sourceName), e));
581 exitCode = 3;
582 }
583 }
584 } catch (MalformedURLException e) {
585 Instance.getTraceHandler()
586 .error(new IOException(trans(StringId.ERR_BAD_URL,
587 sourceName), e));
588 exitCode = 1;
589 }
590
591 return exitCode;
592 }
593
594 /**
595 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
596 *
597 * @param id
598 * the ID to translate
599 *
600 * @return the translated result
601 */
602 private static String trans(StringId id, Object... params) {
603 return Instance.getTrans().getString(id, params);
604 }
605
606 /**
607 * Display the correct syntax of the program to the user to stdout, or an
608 * error message if the syntax used was wrong on stderr.
609 *
610 * @param showHelp
611 * TRUE to show the syntax help, FALSE to show "syntax error"
612 */
613 private static void syntax(boolean showHelp) {
614 if (showHelp) {
615 StringBuilder builder = new StringBuilder();
616 for (SupportType type : SupportType.values()) {
617 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
618 type.getDesc()));
619 builder.append('\n');
620 }
621
622 String typesIn = builder.toString();
623 builder.setLength(0);
624
625 for (OutputType type : OutputType.values()) {
626 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
627 type.getDesc(true)));
628 builder.append('\n');
629 }
630
631 String typesOut = builder.toString();
632
633 System.out.println(trans(StringId.HELP_SYNTAX, typesIn, typesOut));
634 } else {
635 System.err.println(trans(StringId.ERR_SYNTAX));
636 }
637 }
638
639 /**
640 * Set the default reader type for this session only (it can be changed in
641 * the configuration file, too, but this value will override it).
642 *
643 * @param readerTypeString
644 * the type
645 */
646 private static int setReaderType(String readerTypeString) {
647 try {
648 ReaderType readerType = ReaderType.valueOf(readerTypeString
649 .toUpperCase());
650 BasicReader.setDefaultReaderType(readerType);
651 return 0;
652 } catch (IllegalArgumentException e) {
653 Instance.getTraceHandler().error(
654 new IOException("Unknown reader type: " + readerTypeString,
655 e));
656 return 1;
657 }
658 }
659 }