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