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