1 package be
.nikiroo
.fanfix
;
4 import java
.io
.IOException
;
5 import java
.net
.MalformedURLException
;
7 import java
.util
.ArrayList
;
10 import javax
.net
.ssl
.SSLException
;
12 import be
.nikiroo
.fanfix
.bundles
.Config
;
13 import be
.nikiroo
.fanfix
.bundles
.StringId
;
14 import be
.nikiroo
.fanfix
.data
.Chapter
;
15 import be
.nikiroo
.fanfix
.data
.MetaData
;
16 import be
.nikiroo
.fanfix
.data
.Story
;
17 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
18 import be
.nikiroo
.fanfix
.library
.CacheLibrary
;
19 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
20 import be
.nikiroo
.fanfix
.library
.RemoteLibrary
;
21 import be
.nikiroo
.fanfix
.library
.RemoteLibraryServer
;
22 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
23 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
24 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
25 import be
.nikiroo
.fanfix
.reader
.CliReader
;
26 import be
.nikiroo
.fanfix
.searchable
.BasicSearchable
;
27 import be
.nikiroo
.fanfix
.supported
.BasicSupport
;
28 import be
.nikiroo
.fanfix
.supported
.SupportType
;
29 import be
.nikiroo
.utils
.Progress
;
30 import be
.nikiroo
.utils
.Version
;
31 import be
.nikiroo
.utils
.serial
.server
.ServerObject
;
34 * Main program entry point.
39 private enum MainAction
{
40 IMPORT
, EXPORT
, CONVERT
, READ
, READ_URL
, LIST
, HELP
, START
, VERSION
, SERVER
, STOP_SERVER
, REMOTE
, SET_SOURCE
, SET_TITLE
, SET_AUTHOR
, SEARCH
, SEARCH_TAG
44 * Main program entry point.
46 * Known environment variables:
48 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
49 * {@link String}s when possible</li>
50 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
51 * before taking the usual ones; they will also be saved/updated into this
52 * path when the program starts</li>
53 * <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
54 * configuration value with 'true'</li>
58 * <li>--import [URL]: import into library</li>
59 * <li>--export [id] [output_type] [target]: export story to target</li>
60 * <li>--convert [URL] [output_type] [target] (+info): convert URL into
62 * <li>--read [id] ([chapter number]): read the given story from the library
64 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
65 * story, without saving it</li>
66 * <li>--search: list the supported websites (where)</li>
67 * <li>--search [where] [keywords] (page [page]) (item [item]): search on
68 * the supported website and display the given results page of stories it
69 * found, or the story details if asked</li>
70 * <li>--search-tag [where]: list all the tags supported by this website</li>
71 * <li>--search-tag [index 1]... (page [page]) (item [item]): search for the
72 * given stories or subtags, tag by tag, and display information about a
73 * specific page of results or about a specific item if requested</li>
74 * <li>--list ([type]): list the stories present in the library</li>
75 * <li>--set-source [id] [new source]: change the source of the given story</li>
76 * <li>--set-title [id] [new title]: change the title of the given story</li>
77 * <li>--set-author [id] [new author]: change the author of the given story</li>
78 * <li>--version: get the version of the program</li>
79 * <li>--server: start the server mode (see config file for parameters)</li>
80 * <li>--stop-server: stop the running server on this port if any</li>
81 * <li>--remote [key] [host] [port]: use a the given remote library</li>
85 * see method description
87 public static void main(String
[] args
) {
88 // Only one line, but very important:
91 String urlString
= null;
93 String sourceString
= null;
94 String titleString
= null;
95 String authorString
= null;
96 String chapString
= null;
99 MainAction action
= MainAction
.START
;
100 Boolean plusInfo
= null;
103 SupportType searchOn
= null;
104 String search
= null;
105 List
<Integer
> tags
= new ArrayList
<Integer
>();
109 boolean noMoreActions
= false;
112 for (int i
= 0; exitCode
== 0 && i
< args
.length
; i
++) {
113 // Action (--) handling:
114 if (!noMoreActions
&& args
[i
].startsWith("--")) {
115 if (args
[i
].equals("--")) {
116 noMoreActions
= true;
119 action
= MainAction
.valueOf(args
[i
].substring(2)
120 .toUpperCase().replace("-", "_"));
121 } catch (Exception e
) {
122 Instance
.getInstance().getTraceHandler()
123 .error(new IllegalArgumentException("Unknown action: " + args
[i
], e
));
133 if (urlString
== null) {
142 } else if (sourceString
== null) {
143 sourceString
= args
[i
];
144 } else if (target
== null) {
151 if (urlString
== null) {
153 } else if (sourceString
== null) {
154 sourceString
= args
[i
];
155 } else if (target
== null) {
157 } else if (plusInfo
== null) {
158 if ("+info".equals(args
[i
])) {
168 if (sourceString
== null) {
169 sourceString
= args
[i
];
177 } else if (sourceString
== null) {
178 sourceString
= args
[i
];
186 } else if (sourceString
== null) {
187 titleString
= args
[i
];
195 } else if (sourceString
== null) {
196 authorString
= args
[i
];
204 } else if (chapString
== null) {
205 chapString
= args
[i
];
211 if (urlString
== null) {
213 } else if (chapString
== null) {
214 chapString
= args
[i
];
220 if (searchOn
== null) {
221 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
223 if (searchOn
== null) {
224 Instance
.getInstance().getTraceHandler().error("Website not known: <" + args
[i
] + ">");
229 if (BasicSearchable
.getSearchable(searchOn
) == null) {
230 Instance
.getInstance().getTraceHandler().error("Website not supported: " + searchOn
);
234 } else if (search
== null) {
236 } else if (page
!= null && page
== -1) {
238 page
= Integer
.parseInt(args
[i
]);
239 } catch (Exception e
) {
242 } else if (item
!= null && item
== -1) {
244 item
= Integer
.parseInt(args
[i
]);
245 } catch (Exception e
) {
248 } else if (page
== null || item
== null) {
249 if (page
== null && "page".equals(args
[i
])) {
251 } else if (item
== null && "item".equals(args
[i
])) {
261 if (searchOn
== null) {
262 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
264 if (searchOn
== null) {
265 Instance
.getInstance().getTraceHandler().error("Website not known: <" + args
[i
] + ">");
269 if (BasicSearchable
.getSearchable(searchOn
) == null) {
270 Instance
.getInstance().getTraceHandler().error("Website not supported: " + searchOn
);
273 } else if (page
== null && item
== null) {
274 if ("page".equals(args
[i
])) {
276 } else if ("item".equals(args
[i
])) {
280 int index
= Integer
.parseInt(args
[i
]);
282 } catch (NumberFormatException e
) {
283 Instance
.getInstance().getTraceHandler().error("Invalid tag index: " + args
[i
]);
287 } else if (page
!= null && page
== -1) {
289 page
= Integer
.parseInt(args
[i
]);
290 } catch (Exception e
) {
293 } else if (item
!= null && item
== -1) {
295 item
= Integer
.parseInt(args
[i
]);
296 } catch (Exception e
) {
299 } else if (page
== null || item
== null) {
300 if (page
== null && "page".equals(args
[i
])) {
302 } else if (item
== null && "item".equals(args
[i
])) {
315 exitCode
= 255; // not supposed to be selected by user
318 exitCode
= 255; // no arguments for this option
321 exitCode
= 255; // no arguments for this option
324 exitCode
= 255; // no arguments for this option
329 } else if (host
== null) {
331 } else if (port
== null) {
332 port
= Integer
.parseInt(args
[i
]);
334 BasicLibrary lib
= new RemoteLibrary(key
, host
, port
);
335 lib
= new CacheLibrary(
336 Instance
.getInstance().getRemoteDir(host
), lib
,
337 Instance
.getInstance().getUiConfig());
339 Instance
.getInstance().setLibrary(lib
);
341 action
= MainAction
.START
;
349 final Progress mainProgress
= new Progress(0, 80);
350 mainProgress
.addProgressListener(new Progress
.ProgressListener() {
351 private int current
= mainProgress
.getMin();
354 public void progress(Progress progress
, String name
) {
355 int diff
= progress
.getProgress() - current
;
361 StringBuilder builder
= new StringBuilder();
362 for (int i
= 0; i
< diff
; i
++) {
366 System
.err
.print(builder
.toString());
368 if (progress
.isDone()) {
369 System
.err
.println("");
373 Progress pg
= new Progress();
374 mainProgress
.addProgress(pg
, mainProgress
.getMax());
376 VersionCheck updates
= VersionCheck
.check();
377 if (updates
.isNewVersionAvailable()) {
378 // Sent to syserr so not to cause problem if one tries to capture a
379 // story content in text mode
381 .println("A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
382 System
.err
.println("");
383 for (Version v
: updates
.getNewer()) {
384 System
.err
.println("\tVersion " + v
);
385 System
.err
.println("\t-------------");
386 System
.err
.println("");
387 for (String it
: updates
.getChanges().get(v
)) {
388 System
.err
.println("\t- " + it
);
390 System
.err
.println("");
397 exitCode
= imprt(urlString
, pg
);
398 updates
.ok(); // we consider it read
401 exitCode
= export(luid
, sourceString
, target
, pg
);
402 updates
.ok(); // we consider it read
405 exitCode
= convert(urlString
, sourceString
, target
,
406 plusInfo
== null ?
false : plusInfo
, pg
);
407 updates
.ok(); // we consider it read
410 exitCode
= list(sourceString
);
414 Instance
.getInstance().getLibrary().changeSource(luid
, sourceString
, pg
);
415 } catch (IOException e1
) {
416 Instance
.getInstance().getTraceHandler().error(e1
);
422 Instance
.getInstance().getLibrary().changeTitle(luid
, titleString
, pg
);
423 } catch (IOException e1
) {
424 Instance
.getInstance().getTraceHandler().error(e1
);
430 Instance
.getInstance().getLibrary().changeAuthor(luid
, authorString
, pg
);
431 } catch (IOException e1
) {
432 Instance
.getInstance().getTraceHandler().error(e1
);
437 if (luid
== null || luid
.isEmpty()) {
444 BasicLibrary lib
= Instance
.getInstance().getLibrary();
445 exitCode
= read(lib
.getStory(luid
, null), chapString
);
446 } catch (IOException e
) {
447 Instance
.getInstance().getTraceHandler()
448 .error(new IOException("Failed to read book", e
));
454 if (urlString
== null || urlString
.isEmpty()) {
461 BasicSupport support
= BasicSupport
462 .getSupport(BasicReader
.getUrl(urlString
));
463 if (support
== null) {
464 Instance
.getInstance().getTraceHandler()
465 .error("URL not supported: " + urlString
);
470 exitCode
= read(support
.process(null), chapString
);
471 } catch (IOException e
) {
472 Instance
.getInstance().getTraceHandler()
473 .error(new IOException("Failed to read book", e
));
479 page
= page
== null ?
1 : page
;
481 Instance
.getInstance().getTraceHandler().error("Incorrect page number");
486 item
= item
== null ?
0 : item
;
488 Instance
.getInstance().getTraceHandler().error("Incorrect item number");
494 if (searchOn
== null) {
495 new CliReader().listSearchables();
496 } else if (search
!= null) {
498 new CliReader().searchBooksByKeyword(searchOn
, search
, page
,
503 } catch (IOException e1
) {
504 Instance
.getInstance().getTraceHandler().error(e1
);
510 if (searchOn
== null) {
515 page
= page
== null ?
1 : page
;
517 Instance
.getInstance().getTraceHandler().error("Incorrect page number");
522 item
= item
== null ?
0 : item
;
524 Instance
.getInstance().getTraceHandler().error("Incorrect item number");
530 new CliReader().searchBooksByTag(searchOn
, page
, item
,
531 tags
.toArray(new Integer
[] {}));
532 } catch (IOException e1
) {
533 Instance
.getInstance().getTraceHandler().error(e1
);
543 .println(String
.format("Fanfix version %s"
544 + "%nhttps://github.com/nikiroo/fanfix/"
545 + "%n\tWritten by Nikiroo",
546 Version
.getCurrentVersion()));
547 updates
.ok(); // we consider it read
551 new CliReader().listBooks(null);
552 } catch (IOException e
) {
553 Instance
.getInstance().getTraceHandler().error(e
);
558 key
= Instance
.getInstance().getConfig().getString(Config
.SERVER_KEY
);
559 port
= Instance
.getInstance().getConfig().getInteger(Config
.SERVER_PORT
);
561 System
.err
.println("No port configured in the config file");
566 ServerObject server
= new RemoteLibraryServer(key
, port
);
567 server
.setTraceHandler(Instance
.getInstance().getTraceHandler());
569 } catch (IOException e
) {
570 Instance
.getInstance().getTraceHandler().error(e
);
574 // Can be given via "--remote XX XX XX"
576 key
= Instance
.getInstance().getConfig().getString(Config
.SERVER_KEY
);
578 port
= Instance
.getInstance().getConfig().getInteger(Config
.SERVER_PORT
);
581 System
.err
.println("No port given nor configured in the config file");
586 new RemoteLibrary(key
, host
, port
).exit();
587 } catch (SSLException e
) {
588 Instance
.getInstance().getTraceHandler().error(
589 "Bad access key for remote library");
591 } catch (IOException e
) {
592 Instance
.getInstance().getTraceHandler().error(e
);
598 exitCode
= 255; // should not be reachable (REMOTE -> START)
604 Instance
.getInstance().getTempFiles().close();
605 } catch (IOException e
) {
606 Instance
.getInstance().getTraceHandler().error(new IOException("Cannot dispose of the temporary files", e
));
609 if (exitCode
== 255) {
613 System
.exit(exitCode
);
617 * Import the given resource into the {@link LocalLibrary}.
620 * the resource to import
622 * the optional progress reporter
624 * @return the exit return code (0 = success)
626 public static int imprt(String urlString
, Progress pg
) {
628 MetaData meta
= Instance
.getInstance().getLibrary().imprt(BasicReader
.getUrl(urlString
), pg
);
629 System
.out
.println(meta
.getLuid() + ": \"" + meta
.getTitle() + "\" imported.");
630 } catch (IOException e
) {
631 Instance
.getInstance().getTraceHandler().error(e
);
639 * Export the {@link Story} from the {@link LocalLibrary} to the given
645 * the {@link OutputType} to use
649 * the optional progress reporter
651 * @return the exit return code (0 = success)
653 public static int export(String luid
, String typeString
, String target
,
655 OutputType type
= OutputType
.valueOfNullOkUC(typeString
, null);
657 Instance
.getInstance().getTraceHandler().error(new Exception(trans(StringId
.OUTPUT_DESC
, typeString
)));
662 Instance
.getInstance().getLibrary().export(luid
, type
, target
, pg
);
663 } catch (IOException e
) {
664 Instance
.getInstance().getTraceHandler().error(e
);
672 * List the stories of the given source from the {@link LocalLibrary}
673 * (unless NULL is passed, in which case all stories will be listed).
676 * the source to list the known stories of, or NULL to list all
679 * @return the exit return code (0 = success)
681 private static int list(String source
) {
683 new CliReader().listBooks(source
);
684 } catch (IOException e
) {
685 Instance
.getInstance().getTraceHandler().error(e
);
693 * Start the current reader for this {@link Story}.
698 * which {@link Chapter} to read (starting at 1), or NULL to get
699 * the {@link Story} description
701 * @return the exit return code (0 = success)
703 private static int read(Story story
, String chapString
) {
705 if (chapString
!= null) {
707 chap
= Integer
.parseInt(chapString
);
708 } catch (NumberFormatException e
) {
709 Instance
.getInstance().getTraceHandler().error(new IOException(
710 "Chapter number cannot be parsed: " + chapString
, e
));
718 new CliReader().listChapters(story
);
720 new CliReader().printChapter(story
, chap
);
722 } catch (IOException e
) {
723 Instance
.getInstance().getTraceHandler()
724 .error(new IOException("Failed to read book", e
));
728 Instance
.getInstance().getTraceHandler()
729 .error("Cannot find book: " + chapString
);
737 * Convert the {@link Story} into another format.
740 * the source {@link Story} to convert
742 * the {@link OutputType} to convert to
746 * TRUE to also export the cover and info file, even if the given
747 * {@link OutputType} does not usually save them
749 * the optional progress reporter
751 * @return the exit return code (0 = success)
753 public static int convert(String urlString
, String typeString
,
754 String target
, boolean infoCover
, Progress pg
) {
757 Instance
.getInstance().getTraceHandler().trace("Convert: " + urlString
);
758 String sourceName
= urlString
;
760 URL source
= BasicReader
.getUrl(urlString
);
761 sourceName
= source
.toString();
762 if (sourceName
.startsWith("file://")) {
763 sourceName
= sourceName
.substring("file://".length());
766 OutputType type
= OutputType
.valueOfAllOkUC(typeString
, null);
768 Instance
.getInstance().getTraceHandler()
769 .error(new IOException(trans(StringId
.ERR_BAD_OUTPUT_TYPE
, typeString
)));
774 BasicSupport support
= BasicSupport
.getSupport(source
);
776 if (support
!= null) {
777 Instance
.getInstance().getTraceHandler().trace("Support found: " + support
.getClass());
778 Progress pgIn
= new Progress();
779 Progress pgOut
= new Progress();
782 pg
.addProgress(pgIn
, 1);
783 pg
.addProgress(pgOut
, 1);
786 Story story
= support
.process(pgIn
);
788 target
= new File(target
).getAbsolutePath();
789 BasicOutput
.getOutput(type
, infoCover
, infoCover
).process(story
, target
, pgOut
);
790 } catch (IOException e
) {
791 Instance
.getInstance().getTraceHandler()
792 .error(new IOException(trans(StringId
.ERR_SAVING
, target
), e
));
796 Instance
.getInstance().getTraceHandler()
797 .error(new IOException(trans( StringId
.ERR_NOT_SUPPORTED
, source
)));
801 } catch (IOException e
) {
802 Instance
.getInstance().getTraceHandler()
803 .error(new IOException(trans(StringId
.ERR_LOADING
, sourceName
), e
));
807 } catch (MalformedURLException e
) {
808 Instance
.getInstance().getTraceHandler().error(new IOException(trans(StringId
.ERR_BAD_URL
, sourceName
), e
));
816 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
819 * the ID to translate
821 * @return the translated result
823 private static String
trans(StringId id
, Object
... params
) {
824 return Instance
.getInstance().getTrans().getString(id
, params
);
828 * Display the correct syntax of the program to the user to stdout, or an
829 * error message if the syntax used was wrong on stderr.
832 * TRUE to show the syntax help, FALSE to show "syntax error"
834 private static void syntax(boolean showHelp
) {
836 StringBuilder builder
= new StringBuilder();
837 for (SupportType type
: SupportType
.values()) {
838 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
840 builder
.append('\n');
843 String typesIn
= builder
.toString();
844 builder
.setLength(0);
846 for (OutputType type
: OutputType
.values()) {
847 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
848 type
.getDesc(true)));
849 builder
.append('\n');
852 String typesOut
= builder
.toString();
854 System
.out
.println(trans(StringId
.HELP_SYNTAX
, typesIn
, typesOut
));
856 System
.err
.println(trans(StringId
.ERR_SYNTAX
));