Fix remote save, better GUI if bad import url
[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;
62c63b07
NR
27import be.nikiroo.utils.serial.server.ConnectActionClientObject;
28import be.nikiroo.utils.serial.server.ServerObject;
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) {
62c63b07
NR
101 Instance.getTraceHandler().error(
102 new IllegalArgumentException("Unknown action: "
103 + args[i], e));
d0114000
NR
104 exitCode = 255;
105 }
106 }
08fe2e33 107
d0114000
NR
108 continue;
109 }
110
111 switch (action) {
112 case IMPORT:
113 if (urlString == null) {
114 urlString = args[i];
115 } else {
116 exitCode = 255;
117 }
118 break;
119 case EXPORT:
120 if (luid == null) {
121 luid = args[i];
b0e88ebd
NR
122 } else if (sourceString == null) {
123 sourceString = args[i];
d0114000
NR
124 } else if (target == null) {
125 target = args[i];
126 } else {
127 exitCode = 255;
128 }
129 break;
130 case CONVERT:
131 if (urlString == null) {
132 urlString = args[i];
b0e88ebd
NR
133 } else if (sourceString == null) {
134 sourceString = args[i];
d0114000
NR
135 } else if (target == null) {
136 target = args[i];
137 } else if (plusInfo == null) {
138 if ("+info".equals(args[i])) {
139 plusInfo = true;
140 } else {
141 exitCode = 255;
142 }
143 } else {
144 exitCode = 255;
08fe2e33 145 }
d0114000
NR
146 break;
147 case LIST:
b0e88ebd
NR
148 if (sourceString == null) {
149 sourceString = args[i];
d0114000
NR
150 } else {
151 exitCode = 255;
08fe2e33 152 }
d0114000
NR
153 break;
154 case READ:
155 if (luid == null) {
156 luid = args[i];
157 } else if (chapString == null) {
158 chapString = args[i];
159 } else {
160 exitCode = 255;
08fe2e33 161 }
d0114000
NR
162 break;
163 case READ_URL:
164 if (urlString == null) {
165 urlString = args[i];
166 } else if (chapString == null) {
167 chapString = args[i];
168 } else {
169 exitCode = 255;
08fe2e33 170 }
d0114000
NR
171 break;
172 case HELP:
173 exitCode = 255;
174 break;
175 case SET_READER:
7de079f1 176 exitCode = setReaderType(args[i]);
c1873e56 177 action = MainAction.START;
d0114000 178 break;
333f0e7b
NR
179 case START:
180 exitCode = 255; // not supposed to be selected by user
181 break;
39c3c689
NR
182 case VERSION:
183 exitCode = 255; // no arguments for this option
b0e88ebd
NR
184 break;
185 case SERVER:
5e848e6a 186 case STOP_SERVER:
2070ced5
NR
187 if (key == null) {
188 key = args[i];
189 } else if (port == null) {
b0e88ebd
NR
190 port = Integer.parseInt(args[i]);
191 } else {
192 exitCode = 255;
193 }
194 break;
195 case REMOTE:
2070ced5
NR
196 if (key == null) {
197 key = args[i];
198 } else if (host == null) {
b0e88ebd
NR
199 host = args[i];
200 } else if (port == null) {
201 port = Integer.parseInt(args[i]);
ff05b828
NR
202
203 File remoteCacheDir = Instance.getRemoteDir(host);
2070ced5 204 BasicLibrary lib = new RemoteLibrary(key, host, port);
ff05b828
NR
205 lib = new CacheLibrary(remoteCacheDir, lib);
206
207 BasicReader.setDefaultLibrary(lib);
5e848e6a 208
b0e88ebd
NR
209 action = MainAction.START;
210 } else {
211 exitCode = 255;
212 }
213 break;
d0114000
NR
214 }
215 }
216
92fb0719
NR
217 final Progress mainProgress = new Progress(0, 80);
218 mainProgress.addProgressListener(new Progress.ProgressListener() {
219 private int current = mainProgress.getMin();
220
211f7ddb 221 @Override
92fb0719
NR
222 public void progress(Progress progress, String name) {
223 int diff = progress.getProgress() - current;
224 current += diff;
225
226 StringBuilder builder = new StringBuilder();
227 for (int i = 0; i < diff; i++) {
228 builder.append('.');
229 }
230
231 System.err.print(builder.toString());
232
233 if (progress.isDone()) {
234 System.err.println("");
235 }
236 }
237 });
238 Progress pg = new Progress();
239 mainProgress.addProgress(pg, mainProgress.getMax());
240
b42117f1
NR
241 VersionCheck updates = VersionCheck.check();
242 if (updates.isNewVersionAvailable()) {
243 // Sent to syserr so not to cause problem if one tries to capture a
244 // story content in text mode
245 System.err
246 .println("A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
247 System.err.println("");
248 for (Version v : updates.getNewer()) {
249 System.err.println("\tVersion " + v);
250 System.err.println("\t-------------");
251 System.err.println("");
252 for (String item : updates.getChanges().get(v)) {
253 System.err.println("\t- " + item);
254 }
255 System.err.println("");
256 }
257 }
258
d0114000
NR
259 if (exitCode != 255) {
260 switch (action) {
261 case IMPORT:
92fb0719 262 exitCode = imprt(urlString, pg);
b42117f1 263 updates.ok(); // we consider it read
d0114000
NR
264 break;
265 case EXPORT:
b0e88ebd 266 exitCode = export(luid, sourceString, target, pg);
b42117f1 267 updates.ok(); // we consider it read
d0114000
NR
268 break;
269 case CONVERT:
b0e88ebd 270 exitCode = convert(urlString, sourceString, target,
92fb0719 271 plusInfo == null ? false : plusInfo, pg);
b42117f1 272 updates.ok(); // we consider it read
d0114000
NR
273 break;
274 case LIST:
99ccbdf6 275 if (BasicReader.getReader() == null) {
62c63b07
NR
276 Instance.getTraceHandler()
277 .error(new Exception(
278 "No reader type has been configured"));
99ccbdf6
NR
279 exitCode = 10;
280 break;
281 }
b0e88ebd 282 exitCode = list(sourceString);
d0114000
NR
283 break;
284 case READ:
99ccbdf6 285 if (BasicReader.getReader() == null) {
62c63b07
NR
286 Instance.getTraceHandler()
287 .error(new Exception(
288 "No reader type has been configured"));
99ccbdf6
NR
289 exitCode = 10;
290 break;
291 }
d0114000
NR
292 exitCode = read(luid, chapString, true);
293 break;
294 case READ_URL:
99ccbdf6 295 if (BasicReader.getReader() == null) {
62c63b07
NR
296 Instance.getTraceHandler()
297 .error(new Exception(
298 "No reader type has been configured"));
99ccbdf6
NR
299 exitCode = 10;
300 break;
301 }
d0114000
NR
302 exitCode = read(urlString, chapString, false);
303 break;
304 case HELP:
305 syntax(true);
306 exitCode = 0;
307 break;
308 case SET_READER:
b0e88ebd 309 exitCode = 255;
d0114000 310 break;
39c3c689
NR
311 case VERSION:
312 System.out
313 .println(String.format("Fanfix version %s"
314 + "\nhttps://github.com/nikiroo/fanfix/"
315 + "\n\tWritten by Nikiroo",
316 Version.getCurrentVersion()));
b42117f1 317 updates.ok(); // we consider it read
39c3c689 318 break;
333f0e7b 319 case START:
99ccbdf6 320 if (BasicReader.getReader() == null) {
62c63b07
NR
321 Instance.getTraceHandler()
322 .error(new Exception(
323 "No reader type has been configured"));
99ccbdf6
NR
324 exitCode = 10;
325 break;
326 }
b0e88ebd
NR
327 BasicReader.getReader().browse(null);
328 break;
329 case SERVER:
330 if (port == null) {
331 exitCode = 255;
332 break;
333 }
334 try {
62c63b07 335 ServerObject server = new RemoteLibraryServer(key, port);
22b2b942 336 server.setTraceHandler(Instance.getTraceHandler());
b0e88ebd 337 server.start();
b0e88ebd 338 } catch (IOException e) {
62c63b07 339 Instance.getTraceHandler().error(e);
b0e88ebd
NR
340 }
341 return;
5e848e6a
NR
342 case STOP_SERVER:
343 if (port == null) {
344 exitCode = 255;
345 break;
346 }
347
348 try {
2070ced5 349 final String fkey = key;
62c63b07 350 new ConnectActionClientObject(host, port, true) {
5e848e6a
NR
351 @Override
352 public void action(Version serverVersion)
353 throws Exception {
354 try {
2070ced5 355 send(new Object[] { fkey, "EXIT" });
5e848e6a 356 } catch (Exception e) {
62c63b07 357 Instance.getTraceHandler().error(e);
5e848e6a
NR
358 }
359 }
360 }.connect();
361 } catch (IOException e) {
62c63b07 362 Instance.getTraceHandler().error(e);
5e848e6a
NR
363 }
364 break;
b0e88ebd 365 case REMOTE:
99ccbdf6 366 exitCode = 255; // should not be reachable (REMOTE -> START)
333f0e7b 367 break;
08fe2e33
NR
368 }
369 }
370
371 if (exitCode == 255) {
d0114000 372 syntax(false);
08fe2e33
NR
373 }
374
375 if (exitCode != 0) {
376 System.exit(exitCode);
377 }
378 }
379
08fe2e33 380 /**
68e2c6d2 381 * Import the given resource into the {@link LocalLibrary}.
08fe2e33 382 *
d0114000 383 * @param urlString
08fe2e33 384 * the resource to import
92fb0719
NR
385 * @param pg
386 * the optional progress reporter
08fe2e33
NR
387 *
388 * @return the exit return code (0 = success)
389 */
92fb0719 390 public static int imprt(String urlString, Progress pg) {
08fe2e33 391 try {
3b2b638f
NR
392 Story story = Instance.getLibrary().imprt(
393 BasicReader.getUrl(urlString), pg);
08fe2e33
NR
394 System.out.println(story.getMeta().getLuid() + ": \""
395 + story.getMeta().getTitle() + "\" imported.");
396 } catch (IOException e) {
62c63b07 397 Instance.getTraceHandler().error(e);
08fe2e33
NR
398 return 1;
399 }
400
401 return 0;
402 }
403
404 /**
68e2c6d2
NR
405 * Export the {@link Story} from the {@link LocalLibrary} to the given
406 * target.
08fe2e33 407 *
73ce17ef 408 * @param luid
08fe2e33
NR
409 * the story LUID
410 * @param typeString
411 * the {@link OutputType} to use
412 * @param target
413 * the target
92fb0719
NR
414 * @param pg
415 * the optional progress reporter
08fe2e33
NR
416 *
417 * @return the exit return code (0 = success)
418 */
92fb0719
NR
419 public static int export(String luid, String typeString, String target,
420 Progress pg) {
e604986c 421 OutputType type = OutputType.valueOfNullOkUC(typeString, null);
08fe2e33 422 if (type == null) {
62c63b07
NR
423 Instance.getTraceHandler().error(
424 new Exception(trans(StringId.OUTPUT_DESC, typeString)));
08fe2e33
NR
425 return 1;
426 }
427
428 try {
92fb0719 429 Instance.getLibrary().export(luid, type, target, pg);
08fe2e33 430 } catch (IOException e) {
62c63b07 431 Instance.getTraceHandler().error(e);
08fe2e33
NR
432 return 4;
433 }
434
435 return 0;
436 }
437
438 /**
68e2c6d2
NR
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).
08fe2e33 441 *
b0e88ebd
NR
442 * @param source
443 * the source to list the known stories of, or NULL to list all
333f0e7b 444 * stories
08fe2e33
NR
445 *
446 * @return the exit return code (0 = success)
447 */
b0e88ebd 448 private static int list(String source) {
f569d249
NR
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 }
08fe2e33
NR
461 return 0;
462 }
463
464 /**
465 * Start the CLI reader for this {@link Story}.
466 *
467 * @param story
68e2c6d2
NR
468 * the LUID of the {@link Story} in the {@link LocalLibrary}
469 * <b>or</b> the {@link Story} {@link URL}
d0114000 470 * @param chapString
08fe2e33
NR
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 */
d0114000 479 private static int read(String story, String chapString, boolean library) {
08fe2e33 480 try {
e42573a0 481 Reader reader = BasicReader.getReader();
08fe2e33 482 if (library) {
bc2ea776 483 reader.setMeta(story);
08fe2e33 484 } else {
bc2ea776 485 reader.setMeta(BasicReader.getUrl(story), null);
08fe2e33
NR
486 }
487
d0114000
NR
488 if (chapString != null) {
489 try {
bc2ea776
NR
490 reader.setChapter(Integer.parseInt(chapString));
491 reader.read();
d0114000 492 } catch (NumberFormatException e) {
62c63b07
NR
493 Instance.getTraceHandler().error(
494 new IOException("Chapter number cannot be parsed: "
495 + chapString, e));
d0114000
NR
496 return 2;
497 }
08fe2e33
NR
498 } else {
499 reader.read();
500 }
501 } catch (IOException e) {
62c63b07 502 Instance.getTraceHandler().error(e);
08fe2e33
NR
503 return 1;
504 }
505
506 return 0;
507 }
508
509 /**
510 * Convert the {@link Story} into another format.
511 *
d0114000 512 * @param urlString
08fe2e33
NR
513 * the source {@link Story} to convert
514 * @param typeString
515 * the {@link OutputType} to convert to
d0114000 516 * @param target
08fe2e33
NR
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
92fb0719
NR
521 * @param pg
522 * the optional progress reporter
08fe2e33
NR
523 *
524 * @return the exit return code (0 = success)
525 */
d0114000 526 private static int convert(String urlString, String typeString,
92fb0719 527 String target, boolean infoCover, Progress pg) {
08fe2e33
NR
528 int exitCode = 0;
529
d0114000 530 String sourceName = urlString;
08fe2e33 531 try {
3b2b638f 532 URL source = BasicReader.getUrl(urlString);
08fe2e33
NR
533 sourceName = source.toString();
534 if (source.toString().startsWith("file://")) {
535 sourceName = sourceName.substring("file://".length());
536 }
537
e604986c 538 OutputType type = OutputType.valueOfAllOkUC(typeString, null);
08fe2e33 539 if (type == null) {
62c63b07
NR
540 Instance.getTraceHandler().error(
541 new IOException(trans(StringId.ERR_BAD_OUTPUT_TYPE,
542 typeString)));
08fe2e33
NR
543
544 exitCode = 2;
545 } else {
546 try {
547 BasicSupport support = BasicSupport.getSupport(source);
333f0e7b 548
08fe2e33 549 if (support != null) {
bee7dffe
NR
550 Progress pgIn = new Progress();
551 Progress pgOut = new Progress();
552 if (pg != null) {
553 pg.setMax(2);
554 pg.addProgress(pgIn, 1);
555 pg.addProgress(pgOut, 1);
556 }
08fe2e33 557
bee7dffe 558 Story story = support.process(source, pgIn);
08fe2e33 559 try {
d0114000 560 target = new File(target).getAbsolutePath();
08fe2e33 561 BasicOutput.getOutput(type, infoCover).process(
bee7dffe 562 story, target, pgOut);
08fe2e33 563 } catch (IOException e) {
62c63b07
NR
564 Instance.getTraceHandler().error(
565 new IOException(trans(StringId.ERR_SAVING,
566 target), e));
08fe2e33
NR
567 exitCode = 5;
568 }
569 } else {
62c63b07
NR
570 Instance.getTraceHandler().error(
571 new IOException(trans(
572 StringId.ERR_NOT_SUPPORTED, source)));
08fe2e33
NR
573
574 exitCode = 4;
575 }
576 } catch (IOException e) {
62c63b07
NR
577 Instance.getTraceHandler().error(
578 new IOException(trans(StringId.ERR_LOADING,
579 sourceName), e));
08fe2e33
NR
580 exitCode = 3;
581 }
582 }
583 } catch (MalformedURLException e) {
62c63b07
NR
584 Instance.getTraceHandler()
585 .error(new IOException(trans(StringId.ERR_BAD_URL,
586 sourceName), e));
08fe2e33
NR
587 exitCode = 1;
588 }
589
590 return exitCode;
591 }
592
593 /**
594 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
595 *
596 * @param id
597 * the ID to translate
598 *
599 * @return the translated result
600 */
601 private static String trans(StringId id, Object... params) {
602 return Instance.getTrans().getString(id, params);
603 }
604
605 /**
d0114000
NR
606 * Display the correct syntax of the program to the user to stdout, or an
607 * error message if the syntax used was wrong on stderr.
608 *
609 * @param showHelp
610 * TRUE to show the syntax help, FALSE to show "syntax error"
08fe2e33 611 */
d0114000
NR
612 private static void syntax(boolean showHelp) {
613 if (showHelp) {
614 StringBuilder builder = new StringBuilder();
615 for (SupportType type : SupportType.values()) {
616 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
617 type.getDesc()));
618 builder.append('\n');
619 }
08fe2e33 620
d0114000
NR
621 String typesIn = builder.toString();
622 builder.setLength(0);
08fe2e33 623
d0114000
NR
624 for (OutputType type : OutputType.values()) {
625 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
4d205683 626 type.getDesc(true)));
d0114000
NR
627 builder.append('\n');
628 }
08fe2e33 629
d0114000 630 String typesOut = builder.toString();
08fe2e33 631
d0114000
NR
632 System.out.println(trans(StringId.HELP_SYNTAX, typesIn, typesOut));
633 } else {
634 System.err.println(trans(StringId.ERR_SYNTAX));
635 }
636 }
637
638 /**
639 * Set the default reader type for this session only (it can be changed in
640 * the configuration file, too, but this value will override it).
641 *
642 * @param readerTypeString
643 * the type
644 */
645 private static int setReaderType(String readerTypeString) {
646 try {
7de079f1
NR
647 ReaderType readerType = ReaderType.valueOf(readerTypeString
648 .toUpperCase());
d0114000
NR
649 BasicReader.setDefaultReaderType(readerType);
650 return 0;
651 } catch (IllegalArgumentException e) {
62c63b07
NR
652 Instance.getTraceHandler().error(
653 new IOException("Unknown reader type: " + readerTypeString,
654 e));
d0114000
NR
655 return 1;
656 }
08fe2e33
NR
657 }
658}