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