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