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