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