Remote: new command "EXIT"
[nikiroo-utils.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
8 import be.nikiroo.fanfix.bundles.StringId;
9 import be.nikiroo.fanfix.data.Chapter;
10 import be.nikiroo.fanfix.data.Story;
11 import be.nikiroo.fanfix.library.BasicLibrary;
12 import be.nikiroo.fanfix.library.CacheLibrary;
13 import be.nikiroo.fanfix.library.LocalLibrary;
14 import be.nikiroo.fanfix.library.RemoteLibrary;
15 import be.nikiroo.fanfix.library.RemoteLibraryServer;
16 import be.nikiroo.fanfix.output.BasicOutput;
17 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
18 import be.nikiroo.fanfix.reader.BasicReader;
19 import be.nikiroo.fanfix.reader.Reader;
20 import be.nikiroo.fanfix.reader.Reader.ReaderType;
21 import be.nikiroo.fanfix.supported.BasicSupport;
22 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
23 import be.nikiroo.utils.Progress;
24 import be.nikiroo.utils.Version;
25 import be.nikiroo.utils.serial.ConnectActionClient;
26 import be.nikiroo.utils.serial.Server;
27
28 /**
29 * Main program entry point.
30 *
31 * @author niki
32 */
33 public class Main {
34 private enum MainAction {
35 IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, SET_READER, START, VERSION, SERVER, STOP_SERVER, REMOTE,
36 }
37
38 /**
39 * Main program entry point.
40 * <p>
41 * Known environment variables:
42 * <ul>
43 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
44 * {@link String}s when possible</li>
45 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
46 * before taking the usual ones; they will also be saved/updated into this
47 * path when the program starts</li>
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>
59 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
60 * story, without saving it</li>
61 * <li>--list ([type]): list the stories present in the library</li>
62 * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
63 * for this command</li>
64 * <li>--version: get the version of the program</li>
65 * <li>--server [port]: start a server on this port</li>
66 * <li>--stop-server [port]: stop the running server on this port if any</li>
67 * <li>--remote [host] [port]: use a the given remote library</li>
68 * </ul>
69 *
70 * @param args
71 * see method description
72 */
73 public static void main(String[] args) {
74 String urlString = null;
75 String luid = null;
76 String sourceString = null;
77 String chapString = null;
78 String target = null;
79 MainAction action = MainAction.START;
80 Boolean plusInfo = null;
81 String host = null;
82 Integer port = null;
83
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 }
102
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];
117 } else if (sourceString == null) {
118 sourceString = args[i];
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];
128 } else if (sourceString == null) {
129 sourceString = args[i];
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;
140 }
141 break;
142 case LIST:
143 if (sourceString == null) {
144 sourceString = args[i];
145 } else {
146 exitCode = 255;
147 }
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;
156 }
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;
165 }
166 break;
167 case HELP:
168 exitCode = 255;
169 break;
170 case SET_READER:
171 exitCode = setReaderType(args[i]);
172 action = MainAction.START;
173 break;
174 case START:
175 exitCode = 255; // not supposed to be selected by user
176 break;
177 case VERSION:
178 exitCode = 255; // no arguments for this option
179 break;
180 case SERVER:
181 case STOP_SERVER:
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]);
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);
199
200 action = MainAction.START;
201 } else {
202 exitCode = 255;
203 }
204 break;
205 }
206 }
207
208 final Progress mainProgress = new Progress(0, 80);
209 mainProgress.addProgressListener(new Progress.ProgressListener() {
210 private int current = mainProgress.getMin();
211
212 @Override
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
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
250 if (exitCode != 255) {
251 switch (action) {
252 case IMPORT:
253 exitCode = imprt(urlString, pg);
254 updates.ok(); // we consider it read
255 break;
256 case EXPORT:
257 exitCode = export(luid, sourceString, target, pg);
258 updates.ok(); // we consider it read
259 break;
260 case CONVERT:
261 exitCode = convert(urlString, sourceString, target,
262 plusInfo == null ? false : plusInfo, pg);
263 updates.ok(); // we consider it read
264 break;
265 case LIST:
266 if (BasicReader.getReader() == null) {
267 Instance.syserr(new Exception(
268 "No reader type has been configured"));
269 exitCode = 10;
270 break;
271 }
272 exitCode = list(sourceString);
273 break;
274 case READ:
275 if (BasicReader.getReader() == null) {
276 Instance.syserr(new Exception(
277 "No reader type has been configured"));
278 exitCode = 10;
279 break;
280 }
281 exitCode = read(luid, chapString, true);
282 break;
283 case READ_URL:
284 if (BasicReader.getReader() == null) {
285 Instance.syserr(new Exception(
286 "No reader type has been configured"));
287 exitCode = 10;
288 break;
289 }
290 exitCode = read(urlString, chapString, false);
291 break;
292 case HELP:
293 syntax(true);
294 exitCode = 0;
295 break;
296 case SET_READER:
297 exitCode = 255;
298 break;
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()));
305 updates.ok(); // we consider it read
306 break;
307 case START:
308 if (BasicReader.getReader() == null) {
309 Instance.syserr(new Exception(
310 "No reader type has been configured"));
311 exitCode = 10;
312 break;
313 }
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;
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;
351 case REMOTE:
352 exitCode = 255; // should not be reachable (REMOTE -> START)
353 break;
354 }
355 }
356
357 if (exitCode == 255) {
358 syntax(false);
359 }
360
361 if (exitCode != 0) {
362 System.exit(exitCode);
363 }
364 }
365
366 /**
367 * Import the given resource into the {@link LocalLibrary}.
368 *
369 * @param urlString
370 * the resource to import
371 * @param pg
372 * the optional progress reporter
373 *
374 * @return the exit return code (0 = success)
375 */
376 public static int imprt(String urlString, Progress pg) {
377 try {
378 Story story = Instance.getLibrary().imprt(
379 BasicReader.getUrl(urlString), pg);
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 /**
391 * Export the {@link Story} from the {@link LocalLibrary} to the given
392 * target.
393 *
394 * @param luid
395 * the story LUID
396 * @param typeString
397 * the {@link OutputType} to use
398 * @param target
399 * the target
400 * @param pg
401 * the optional progress reporter
402 *
403 * @return the exit return code (0 = success)
404 */
405 public static int export(String luid, String typeString, String target,
406 Progress pg) {
407 OutputType type = OutputType.valueOfNullOkUC(typeString, null);
408 if (type == null) {
409 Instance.syserr(new Exception(trans(StringId.OUTPUT_DESC,
410 typeString)));
411 return 1;
412 }
413
414 try {
415 Instance.getLibrary().export(luid, type, target, pg);
416 } catch (IOException e) {
417 Instance.syserr(e);
418 return 4;
419 }
420
421 return 0;
422 }
423
424 /**
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).
427 *
428 * @param source
429 * the source to list the known stories of, or NULL to list all
430 * stories
431 *
432 * @return the exit return code (0 = success)
433 */
434 private static int list(String source) {
435 BasicReader.getReader().browse(source);
436 return 0;
437 }
438
439 /**
440 * Start the CLI reader for this {@link Story}.
441 *
442 * @param story
443 * the LUID of the {@link Story} in the {@link LocalLibrary}
444 * <b>or</b> the {@link Story} {@link URL}
445 * @param chapString
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 */
454 private static int read(String story, String chapString, boolean library) {
455 try {
456 Reader reader = BasicReader.getReader();
457 if (library) {
458 reader.setMeta(story);
459 } else {
460 reader.setMeta(BasicReader.getUrl(story), null);
461 }
462
463 if (chapString != null) {
464 try {
465 reader.setChapter(Integer.parseInt(chapString));
466 reader.read();
467 } catch (NumberFormatException e) {
468 Instance.syserr(new IOException(
469 "Chapter number cannot be parsed: " + chapString, e));
470 return 2;
471 }
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 *
486 * @param urlString
487 * the source {@link Story} to convert
488 * @param typeString
489 * the {@link OutputType} to convert to
490 * @param target
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
495 * @param pg
496 * the optional progress reporter
497 *
498 * @return the exit return code (0 = success)
499 */
500 private static int convert(String urlString, String typeString,
501 String target, boolean infoCover, Progress pg) {
502 int exitCode = 0;
503
504 String sourceName = urlString;
505 try {
506 URL source = BasicReader.getUrl(urlString);
507 sourceName = source.toString();
508 if (source.toString().startsWith("file://")) {
509 sourceName = sourceName.substring("file://".length());
510 }
511
512 OutputType type = OutputType.valueOfAllOkUC(typeString, null);
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);
521
522 if (support != null) {
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 }
530
531 Story story = support.process(source, pgIn);
532 try {
533 target = new File(target).getAbsolutePath();
534 BasicOutput.getOutput(type, infoCover).process(
535 story, target, pgOut);
536 } catch (IOException e) {
537 Instance.syserr(new IOException(trans(
538 StringId.ERR_SAVING, target), e));
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 /**
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"
580 */
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 }
589
590 String typesIn = builder.toString();
591 builder.setLength(0);
592
593 for (OutputType type : OutputType.values()) {
594 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
595 type.getDesc(true)));
596 builder.append('\n');
597 }
598
599 String typesOut = builder.toString();
600
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 {
616 ReaderType readerType = ReaderType.valueOf(readerTypeString
617 .toUpperCase());
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 }
625 }
626 }