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