2e8b6d17cdc7dc99b17353df55aefd469ef4a0a8
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 new Main().start(args
);
92 * Start the default handling for the application.
94 * If specific actions were asked (with correct parameters), they will be
95 * forwarded to the different protected methods that you can override.
97 * At the end of the method, {@link Main#exit(int)} will be called; by
98 * default, it calls {@link System#exit(int)} if the status is not 0.
101 * the arguments received from the system
103 public void start(String
[] args
) {
104 // Only one line, but very important:
107 String urlString
= null;
109 String sourceString
= null;
110 String titleString
= null;
111 String authorString
= null;
112 String chapString
= null;
113 String target
= null;
115 MainAction action
= MainAction
.START
;
116 Boolean plusInfo
= null;
119 SupportType searchOn
= null;
120 String search
= null;
121 List
<Integer
> tags
= new ArrayList
<Integer
>();
125 boolean noMoreActions
= false;
128 for (int i
= 0; exitCode
== 0 && i
< args
.length
; i
++) {
129 // Action (--) handling:
130 if (!noMoreActions
&& args
[i
].startsWith("--")) {
131 if (args
[i
].equals("--")) {
132 noMoreActions
= true;
135 action
= MainAction
.valueOf(args
[i
].substring(2)
136 .toUpperCase().replace("-", "_"));
137 } catch (Exception e
) {
138 Instance
.getInstance().getTraceHandler()
139 .error(new IllegalArgumentException("Unknown action: " + args
[i
], e
));
149 if (urlString
== null) {
158 } else if (sourceString
== null) {
159 sourceString
= args
[i
];
160 } else if (target
== null) {
167 if (urlString
== null) {
169 } else if (sourceString
== null) {
170 sourceString
= args
[i
];
171 } else if (target
== null) {
173 } else if (plusInfo
== null) {
174 if ("+info".equals(args
[i
])) {
184 if (sourceString
== null) {
185 sourceString
= args
[i
];
193 } else if (sourceString
== null) {
194 sourceString
= args
[i
];
202 } else if (sourceString
== null) {
203 titleString
= args
[i
];
211 } else if (sourceString
== null) {
212 authorString
= args
[i
];
220 } else if (chapString
== null) {
221 chapString
= args
[i
];
227 if (urlString
== null) {
229 } else if (chapString
== null) {
230 chapString
= args
[i
];
236 if (searchOn
== null) {
237 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
239 if (searchOn
== null) {
240 Instance
.getInstance().getTraceHandler().error("Website not known: <" + args
[i
] + ">");
245 if (BasicSearchable
.getSearchable(searchOn
) == null) {
246 Instance
.getInstance().getTraceHandler().error("Website not supported: " + searchOn
);
250 } else if (search
== null) {
252 } else if (page
!= null && page
== -1) {
254 page
= Integer
.parseInt(args
[i
]);
255 } catch (Exception e
) {
258 } else if (item
!= null && item
== -1) {
260 item
= Integer
.parseInt(args
[i
]);
261 } catch (Exception e
) {
264 } else if (page
== null || item
== null) {
265 if (page
== null && "page".equals(args
[i
])) {
267 } else if (item
== null && "item".equals(args
[i
])) {
277 if (searchOn
== null) {
278 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
280 if (searchOn
== null) {
281 Instance
.getInstance().getTraceHandler().error("Website not known: <" + args
[i
] + ">");
285 if (BasicSearchable
.getSearchable(searchOn
) == null) {
286 Instance
.getInstance().getTraceHandler().error("Website not supported: " + searchOn
);
289 } else if (page
== null && item
== null) {
290 if ("page".equals(args
[i
])) {
292 } else if ("item".equals(args
[i
])) {
296 int index
= Integer
.parseInt(args
[i
]);
298 } catch (NumberFormatException e
) {
299 Instance
.getInstance().getTraceHandler().error("Invalid tag index: " + args
[i
]);
303 } else if (page
!= null && page
== -1) {
305 page
= Integer
.parseInt(args
[i
]);
306 } catch (Exception e
) {
309 } else if (item
!= null && item
== -1) {
311 item
= Integer
.parseInt(args
[i
]);
312 } catch (Exception e
) {
315 } else if (page
== null || item
== null) {
316 if (page
== null && "page".equals(args
[i
])) {
318 } else if (item
== null && "item".equals(args
[i
])) {
331 exitCode
= 255; // not supposed to be selected by user
334 exitCode
= 255; // no arguments for this option
337 exitCode
= 255; // no arguments for this option
340 exitCode
= 255; // no arguments for this option
345 } else if (host
== null) {
347 } else if (port
== null) {
348 port
= Integer
.parseInt(args
[i
]);
350 BasicLibrary lib
= new RemoteLibrary(key
, host
, port
);
351 lib
= new CacheLibrary(
352 Instance
.getInstance().getRemoteDir(host
), lib
,
353 Instance
.getInstance().getUiConfig());
355 Instance
.getInstance().setLibrary(lib
);
357 action
= MainAction
.START
;
365 final Progress mainProgress
= new Progress(0, 80);
366 mainProgress
.addProgressListener(new Progress
.ProgressListener() {
367 private int current
= mainProgress
.getMin();
370 public void progress(Progress progress
, String name
) {
371 int diff
= progress
.getProgress() - current
;
377 StringBuilder builder
= new StringBuilder();
378 for (int i
= 0; i
< diff
; i
++) {
382 System
.err
.print(builder
.toString());
384 if (progress
.isDone()) {
385 System
.err
.println("");
389 Progress pg
= new Progress();
390 mainProgress
.addProgress(pg
, mainProgress
.getMax());
392 VersionCheck updates
= checkUpdates();
398 updates
.ok(); // we consider it read
401 exitCode
= imprt(BasicReader
.getUrl(urlString
), pg
);
402 } catch (MalformedURLException e
) {
403 Instance
.getInstance().getTraceHandler().error(e
);
410 updates
.ok(); // we consider it read
412 OutputType exportType
= OutputType
.valueOfNullOkUC(sourceString
, null);
413 if (exportType
== null) {
414 Instance
.getInstance().getTraceHandler().error(new Exception(trans(StringId
.OUTPUT_DESC
, sourceString
)));
419 exitCode
= export(luid
, exportType
, target
, pg
);
424 updates
.ok(); // we consider it read
426 OutputType convertType
= OutputType
.valueOfAllOkUC(sourceString
, null);
427 if (convertType
== null) {
428 Instance
.getInstance().getTraceHandler()
429 .error(new IOException(trans(StringId
.ERR_BAD_OUTPUT_TYPE
, sourceString
)));
435 exitCode
= convert(urlString
, convertType
, target
,
436 plusInfo
== null ?
false : plusInfo
, pg
);
440 exitCode
= list(sourceString
);
444 Instance
.getInstance().getLibrary().changeSource(luid
, sourceString
, pg
);
445 } catch (IOException e1
) {
446 Instance
.getInstance().getTraceHandler().error(e1
);
452 Instance
.getInstance().getLibrary().changeTitle(luid
, titleString
, pg
);
453 } catch (IOException e1
) {
454 Instance
.getInstance().getTraceHandler().error(e1
);
460 Instance
.getInstance().getLibrary().changeAuthor(luid
, authorString
, pg
);
461 } catch (IOException e1
) {
462 Instance
.getInstance().getTraceHandler().error(e1
);
467 if (luid
== null || luid
.isEmpty()) {
475 if (chapString
!= null) {
477 chap
= Integer
.parseInt(chapString
);
478 } catch (NumberFormatException e
) {
479 Instance
.getInstance().getTraceHandler().error(new IOException(
480 "Chapter number cannot be parsed: " + chapString
, e
));
486 BasicLibrary lib
= Instance
.getInstance().getLibrary();
487 exitCode
= read(lib
.getStory(luid
, null), chap
);
488 } catch (IOException e
) {
489 Instance
.getInstance().getTraceHandler()
490 .error(new IOException("Failed to read book", e
));
496 if (urlString
== null || urlString
.isEmpty()) {
504 if (chapString
!= null) {
506 chap
= Integer
.parseInt(chapString
);
507 } catch (NumberFormatException e
) {
508 Instance
.getInstance().getTraceHandler().error(new IOException(
509 "Chapter number cannot be parsed: " + chapString
, e
));
515 BasicSupport support
= BasicSupport
516 .getSupport(BasicReader
.getUrl(urlString
));
517 if (support
== null) {
518 Instance
.getInstance().getTraceHandler()
519 .error("URL not supported: " + urlString
);
524 exitCode
= read(support
.process(null), chap
);
525 } catch (IOException e
) {
526 Instance
.getInstance().getTraceHandler()
527 .error(new IOException("Failed to read book", e
));
533 page
= page
== null ?
1 : page
;
535 Instance
.getInstance().getTraceHandler().error("Incorrect page number");
540 item
= item
== null ?
0 : item
;
542 Instance
.getInstance().getTraceHandler().error("Incorrect item number");
547 if (searchOn
== null) {
550 } catch (IOException e
) {
551 Instance
.getInstance().getTraceHandler().error(e
);
554 } else if (search
!= null) {
556 searchKeywords(searchOn
, search
, page
, item
);
557 } catch (IOException e
) {
558 Instance
.getInstance().getTraceHandler().error(e
);
567 if (searchOn
== null) {
572 page
= page
== null ?
1 : page
;
574 Instance
.getInstance().getTraceHandler().error("Incorrect page number");
579 item
= item
== null ?
0 : item
;
581 Instance
.getInstance().getTraceHandler().error("Incorrect item number");
587 searchTags(searchOn
, page
, item
,
588 tags
.toArray(new Integer
[] {}));
589 } catch (IOException e
) {
590 Instance
.getInstance().getTraceHandler().error(e
);
600 updates
.ok(); // we consider it read
603 .println(String
.format("Fanfix version %s"
604 + "%nhttps://github.com/nikiroo/fanfix/"
605 + "%n\tWritten by Nikiroo",
606 Version
.getCurrentVersion()));
611 } catch (IOException e
) {
612 Instance
.getInstance().getTraceHandler().error(e
);
617 key
= Instance
.getInstance().getConfig().getString(Config
.SERVER_KEY
);
618 port
= Instance
.getInstance().getConfig().getInteger(Config
.SERVER_PORT
);
620 System
.err
.println("No port configured in the config file");
625 startServer(key
, port
);
626 } catch (IOException e
) {
627 Instance
.getInstance().getTraceHandler().error(e
);
632 // Can be given via "--remote XX XX XX"
634 key
= Instance
.getInstance().getConfig().getString(Config
.SERVER_KEY
);
636 port
= Instance
.getInstance().getConfig().getInteger(Config
.SERVER_PORT
);
639 System
.err
.println("No port given nor configured in the config file");
644 stopServer(key
, host
, port
);
645 } catch (SSLException e
) {
646 Instance
.getInstance().getTraceHandler().error(
647 "Bad access key for remote library");
649 } catch (IOException e
) {
650 Instance
.getInstance().getTraceHandler().error(e
);
656 exitCode
= 255; // should not be reachable (REMOTE -> START)
662 Instance
.getInstance().getTempFiles().close();
663 } catch (IOException e
) {
664 Instance
.getInstance().getTraceHandler().error(new IOException(
665 "Cannot dispose of the temporary files", e
));
668 if (exitCode
== 255) {
676 * A normal invocation of the program (without parameters or at least
677 * without "action" parameters).
679 * You will probably want to override that one if you offer a user
682 * @throws IOException
683 * in case of I/O error
685 protected void start() throws IOException
{
686 new CliReader().listBooks(null);
690 * Will check if updates are available, synchronously.
692 * For this, it will simply forward the call to
693 * {@link Main#checkUpdates(String)} with a value of "nikiroo/fanfix".
695 * You may want to override it so you call the forward method with the right
696 * parameters (or also if you want it to be asynchronous).
698 * @return the newer version information or NULL if nothing new
700 protected VersionCheck
checkUpdates() {
701 return checkUpdates("nikiroo/fanfix");
705 * Will check if updates are available on a specific GitHub project.
707 * Will be called by {@link Main#checkUpdates()}, but if you override that
708 * one you mall call it with another project.
710 * @param githubProject
711 * the GitHub project, for instance "nikiroo/fanfix"
713 * @return the newer version information or NULL if nothing new
715 protected VersionCheck
checkUpdates(String githubProject
) {
716 VersionCheck updates
= VersionCheck
.check(githubProject
);
717 if (updates
.isNewVersionAvailable()) {
718 notifyUpdates(updates
);
726 * Notify the user about available updates.
728 * Will only be called when a version is available.
730 * Note that you can call {@link VersionCheck#ok()} on it if the user has
731 * read the information (by default, it is marked read only on certain other
735 * the new version information
737 protected void notifyUpdates(VersionCheck updates
) {
738 // Sent to syserr so not to cause problem if one tries to capture a
739 // story content in text mode
741 "A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
742 System
.err
.println("");
743 for (Version v
: updates
.getNewer()) {
744 System
.err
.println("\tVersion " + v
);
745 System
.err
.println("\t-------------");
746 System
.err
.println("");
747 for (String it
: updates
.getChanges().get(v
)) {
748 System
.err
.println("\t- " + it
);
750 System
.err
.println("");
755 * Import the given resource into the {@link LocalLibrary}.
758 * the resource to import
760 * the optional progress reporter
762 * @return the exit return code (0 = success)
764 protected static int imprt(URL url
, Progress pg
) {
766 MetaData meta
= Instance
.getInstance().getLibrary().imprt(url
, pg
);
767 System
.out
.println(meta
.getLuid() + ": \"" + meta
.getTitle() + "\" imported.");
768 } catch (IOException e
) {
769 Instance
.getInstance().getTraceHandler().error(e
);
777 * Export the {@link Story} from the {@link LocalLibrary} to the given
783 * the {@link OutputType} to use
787 * the optional progress reporter
789 * @return the exit return code (0 = success)
791 protected static int export(String luid
, OutputType type
, String target
,
794 Instance
.getInstance().getLibrary().export(luid
, type
, target
, pg
);
795 } catch (IOException e
) {
796 Instance
.getInstance().getTraceHandler().error(e
);
804 * List the stories of the given source from the {@link LocalLibrary}
805 * (unless NULL is passed, in which case all stories will be listed).
808 * the source to list the known stories of, or NULL to list all
811 * @return the exit return code (0 = success)
813 protected int list(String source
) {
815 new CliReader().listBooks(source
);
816 } catch (IOException e
) {
817 Instance
.getInstance().getTraceHandler().error(e
);
825 * Start the current reader for this {@link Story}.
830 * which {@link Chapter} to read (starting at 1), or NULL to get
831 * the {@link Story} description
833 * @return the exit return code (0 = success)
835 protected int read(Story story
, Integer chap
) {
839 new CliReader().listChapters(story
);
841 new CliReader().printChapter(story
, chap
);
843 } catch (IOException e
) {
844 Instance
.getInstance().getTraceHandler()
845 .error(new IOException("Failed to read book", e
));
849 Instance
.getInstance().getTraceHandler()
850 .error("Cannot find book: " + story
);
858 * Convert the {@link Story} into another format.
861 * the source {@link Story} to convert
863 * the {@link OutputType} to convert to
867 * TRUE to also export the cover and info file, even if the given
868 * {@link OutputType} does not usually save them
870 * the optional progress reporter
872 * @return the exit return code (0 = success)
874 protected int convert(String urlString
, OutputType type
,
875 String target
, boolean infoCover
, Progress pg
) {
878 Instance
.getInstance().getTraceHandler().trace("Convert: " + urlString
);
879 String sourceName
= urlString
;
881 URL source
= BasicReader
.getUrl(urlString
);
882 sourceName
= source
.toString();
883 if (sourceName
.startsWith("file://")) {
884 sourceName
= sourceName
.substring("file://".length());
888 BasicSupport support
= BasicSupport
.getSupport(source
);
890 if (support
!= null) {
891 Instance
.getInstance().getTraceHandler()
892 .trace("Support found: " + support
.getClass());
893 Progress pgIn
= new Progress();
894 Progress pgOut
= new Progress();
897 pg
.addProgress(pgIn
, 1);
898 pg
.addProgress(pgOut
, 1);
901 Story story
= support
.process(pgIn
);
903 target
= new File(target
).getAbsolutePath();
904 BasicOutput
.getOutput(type
, infoCover
, infoCover
)
905 .process(story
, target
, pgOut
);
906 } catch (IOException e
) {
907 Instance
.getInstance().getTraceHandler()
908 .error(new IOException(
909 trans(StringId
.ERR_SAVING
, target
), e
));
913 Instance
.getInstance().getTraceHandler()
914 .error(new IOException(
915 trans(StringId
.ERR_NOT_SUPPORTED
, source
)));
919 } catch (IOException e
) {
920 Instance
.getInstance().getTraceHandler().error(new IOException(
921 trans(StringId
.ERR_LOADING
, sourceName
), e
));
924 } catch (MalformedURLException e
) {
925 Instance
.getInstance().getTraceHandler().error(new IOException(trans(StringId
.ERR_BAD_URL
, sourceName
), e
));
933 * Display the correct syntax of the program to the user to stdout, or an
934 * error message if the syntax used was wrong on stderr.
937 * TRUE to show the syntax help, FALSE to show "syntax error"
939 protected void syntax(boolean showHelp
) {
941 StringBuilder builder
= new StringBuilder();
942 for (SupportType type
: SupportType
.values()) {
943 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
945 builder
.append('\n');
948 String typesIn
= builder
.toString();
949 builder
.setLength(0);
951 for (OutputType type
: OutputType
.values()) {
952 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
953 type
.getDesc(true)));
954 builder
.append('\n');
957 String typesOut
= builder
.toString();
959 System
.out
.println(trans(StringId
.HELP_SYNTAX
, typesIn
, typesOut
));
961 System
.err
.println(trans(StringId
.ERR_SYNTAX
));
966 * Starts a search operation (i.e., list the available web sites we can
969 * @throws IOException
970 * in case of I/O errors
972 protected void search() throws IOException
{
973 new CliReader().listSearchables();
977 * Search for books by keywords on the given supported web site.
980 * the web site to search on
982 * the keyword to look for
984 * the page of results to get, or 0 to inquire about the number
987 * the index of the book we are interested by, or 0 to query
988 * about how many books are in that page of results
990 * @throws IOException
991 * in case of I/O error
993 protected void searchKeywords(SupportType searchOn
, String search
,
994 int page
, Integer item
) throws IOException
{
995 new CliReader().searchBooksByKeyword(searchOn
, search
, page
, item
);
999 * Search for books by tags on the given supported web site.
1002 * the web site to search on
1004 * the page of results to get, or 0 to inquire about the number
1007 * the index of the book we are interested by, or 0 to query
1008 * about how many books are in that page of results
1010 * the tags to look for
1012 * @throws IOException
1013 * in case of I/O error
1015 protected void searchTags(SupportType searchOn
, Integer page
, Integer item
,
1016 Integer
[] tags
) throws IOException
{
1017 new CliReader().searchBooksByTag(searchOn
, page
, item
, tags
);
1021 * Start a Fanfix server.
1024 * the key taht will be needed to contact the Fanfix server
1026 * the port on which to run
1028 * @throws IOException
1029 * in case of I/O errors
1030 * @throws SSLException
1031 * when the key was not accepted
1033 private void startServer(String key
, int port
) throws IOException
{
1034 ServerObject server
= new RemoteLibraryServer(key
, port
);
1035 server
.setTraceHandler(Instance
.getInstance().getTraceHandler());
1040 * Stop a running Fanfix server.
1043 * the key to contact the Fanfix server
1045 * the host on which it runs (NULL means localhost)
1047 * the port on which it runs
1049 * @throws IOException
1050 * in case of I/O errors
1051 * @throws SSLException
1052 * when the key was not accepted
1054 private void stopServer(
1055 String key
, String host
, Integer port
)
1056 throws IOException
, SSLException
{
1057 new RemoteLibrary(key
, host
, port
).exit();
1061 * We are done and ready to exit.
1063 * By default, it will call {@link System#exit(int)} if the status is not 0.
1068 protected void exit(int status
) {
1070 System
.exit(status
);
1075 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
1078 * the ID to translate
1080 * @return the translated result
1082 static private String
trans(StringId id
, Object
... params
) {
1083 return Instance
.getInstance().getTrans().getString(id
, params
);