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