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
.ConfigBundle
;
14 import be
.nikiroo
.fanfix
.bundles
.StringId
;
15 import be
.nikiroo
.fanfix
.bundles
.StringIdBundle
;
16 import be
.nikiroo
.fanfix
.data
.Chapter
;
17 import be
.nikiroo
.fanfix
.data
.MetaData
;
18 import be
.nikiroo
.fanfix
.data
.Story
;
19 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
20 import be
.nikiroo
.fanfix
.library
.CacheLibrary
;
21 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
22 import be
.nikiroo
.fanfix
.library
.RemoteLibrary
;
23 import be
.nikiroo
.fanfix
.library
.RemoteLibraryServer
;
24 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
25 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
26 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
27 import be
.nikiroo
.fanfix
.reader
.Reader
;
28 import be
.nikiroo
.fanfix
.reader
.Reader
.ReaderType
;
29 import be
.nikiroo
.fanfix
.searchable
.BasicSearchable
;
30 import be
.nikiroo
.fanfix
.supported
.BasicSupport
;
31 import be
.nikiroo
.fanfix
.supported
.SupportType
;
32 import be
.nikiroo
.utils
.Progress
;
33 import be
.nikiroo
.utils
.Version
;
34 import be
.nikiroo
.utils
.resources
.Bundles
;
35 import be
.nikiroo
.utils
.resources
.TransBundle
;
36 import be
.nikiroo
.utils
.serial
.server
.ServerObject
;
39 * Main program entry point.
44 private enum MainAction
{
45 IMPORT
, EXPORT
, CONVERT
, READ
, READ_URL
, LIST
, HELP
, SET_READER
, START
, VERSION
, SERVER
, STOP_SERVER
, REMOTE
, SET_SOURCE
, SET_TITLE
, SET_AUTHOR
, SEARCH
, SEARCH_TAG
49 * Main program entry point.
51 * Known environment variables:
53 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
54 * {@link String}s when possible</li>
55 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
56 * before taking the usual ones; they will also be saved/updated into this
57 * path when the program starts</li>
58 * <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
59 * configuration value with 'true'</li>
63 * <li>--import [URL]: import into library</li>
64 * <li>--export [id] [output_type] [target]: export story to target</li>
65 * <li>--convert [URL] [output_type] [target] (+info): convert URL into
67 * <li>--read [id] ([chapter number]): read the given story from the library
69 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
70 * story, without saving it</li>
71 * <li>--search: list the supported websites (where)</li>
72 * <li>--search [where] [keywords] (page [page]) (item [item]): search on
73 * the supported website and display the given results page of stories it
74 * found, or the story details if asked</li>
75 * <li>--search-tag [where]: list all the tags supported by this website</li>
76 * <li>--search-tag [index 1]... (page [page]) (item [item]): search for the
77 * given stories or subtags, tag by tag, and display information about a
78 * specific page of results or about a specific item if requested</li>
79 * <li>--list ([type]): list the stories present in the library</li>
80 * <li>--set-source [id] [new source]: change the source of the given story</li>
81 * <li>--set-title [id] [new title]: change the title of the given story</li>
82 * <li>--set-author [id] [new author]: change the author of the given story</li>
83 * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
84 * for this command</li>
85 * <li>--version: get the version of the program</li>
86 * <li>--server: start the server mode (see config file for parameters)</li>
87 * <li>--stop-server: stop the running server on this port if any</li>
88 * <li>--remote [key] [host] [port]: use a the given remote library</li>
92 * see method description
94 public static void main(String
[] args
) {
95 // Only one line, but very important:
98 String urlString
= null;
100 String sourceString
= null;
101 String titleString
= null;
102 String authorString
= null;
103 String chapString
= null;
104 String target
= null;
106 MainAction action
= MainAction
.START
;
107 Boolean plusInfo
= null;
110 SupportType searchOn
= null;
111 String search
= null;
112 List
<Integer
> tags
= new ArrayList
<Integer
>();
116 boolean noMoreActions
= false;
119 for (int i
= 0; exitCode
== 0 && i
< args
.length
; i
++) {
120 // Action (--) handling:
121 if (!noMoreActions
&& args
[i
].startsWith("--")) {
122 if (args
[i
].equals("--")) {
123 noMoreActions
= true;
126 action
= MainAction
.valueOf(args
[i
].substring(2)
127 .toUpperCase().replace("-", "_"));
128 } catch (Exception e
) {
129 Instance
.getTraceHandler().error(
130 new IllegalArgumentException("Unknown action: "
141 if (urlString
== null) {
150 } else if (sourceString
== null) {
151 sourceString
= args
[i
];
152 } else if (target
== null) {
159 if (urlString
== null) {
161 } else if (sourceString
== null) {
162 sourceString
= args
[i
];
163 } else if (target
== null) {
165 } else if (plusInfo
== null) {
166 if ("+info".equals(args
[i
])) {
176 if (sourceString
== null) {
177 sourceString
= args
[i
];
185 } else if (sourceString
== null) {
186 sourceString
= args
[i
];
194 } else if (sourceString
== null) {
195 titleString
= args
[i
];
203 } else if (sourceString
== null) {
204 authorString
= args
[i
];
212 } else if (chapString
== null) {
213 chapString
= args
[i
];
219 if (urlString
== null) {
221 } else if (chapString
== null) {
222 chapString
= args
[i
];
228 if (searchOn
== null) {
229 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
231 if (searchOn
== null) {
232 Instance
.getTraceHandler().error(
233 "Website not known: <" + args
[i
] + ">");
238 if (BasicSearchable
.getSearchable(searchOn
) == null) {
239 Instance
.getTraceHandler().error(
240 "Website not supported: " + searchOn
);
244 } else if (search
== null) {
246 } else if (page
!= null && page
== -1) {
248 page
= Integer
.parseInt(args
[i
]);
249 } catch (Exception e
) {
252 } else if (item
!= null && item
== -1) {
254 item
= Integer
.parseInt(args
[i
]);
255 } catch (Exception e
) {
258 } else if (page
== null || item
== null) {
259 if (page
== null && "page".equals(args
[i
])) {
261 } else if (item
== null && "item".equals(args
[i
])) {
271 if (searchOn
== null) {
272 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
274 if (searchOn
== null) {
275 Instance
.getTraceHandler().error(
276 "Website not known: <" + args
[i
] + ">");
280 if (BasicSearchable
.getSearchable(searchOn
) == null) {
281 Instance
.getTraceHandler().error(
282 "Website not supported: " + searchOn
);
285 } else if (page
== null && item
== null) {
286 if ("page".equals(args
[i
])) {
288 } else if ("item".equals(args
[i
])) {
292 int index
= Integer
.parseInt(args
[i
]);
294 } catch (NumberFormatException e
) {
295 Instance
.getTraceHandler().error(
296 "Invalid tag index: " + args
[i
]);
300 } else if (page
!= null && page
== -1) {
302 page
= Integer
.parseInt(args
[i
]);
303 } catch (Exception e
) {
306 } else if (item
!= null && item
== -1) {
308 item
= Integer
.parseInt(args
[i
]);
309 } catch (Exception e
) {
312 } else if (page
== null || item
== null) {
313 if (page
== null && "page".equals(args
[i
])) {
315 } else if (item
== null && "item".equals(args
[i
])) {
328 exitCode
= setReaderType(args
[i
]);
329 action
= MainAction
.START
;
332 exitCode
= 255; // not supposed to be selected by user
335 exitCode
= 255; // no arguments for this option
338 exitCode
= 255; // no arguments for this option
341 exitCode
= 255; // no arguments for this option
346 } else if (host
== null) {
348 } else if (port
== null) {
349 port
= Integer
.parseInt(args
[i
]);
351 BasicLibrary lib
= new RemoteLibrary(key
, host
, port
);
352 lib
= new CacheLibrary(Instance
.getRemoteDir(host
), lib
);
354 BasicReader
.setDefaultLibrary(lib
);
356 action
= MainAction
.START
;
364 final Progress mainProgress
= new Progress(0, 80);
365 mainProgress
.addProgressListener(new Progress
.ProgressListener() {
366 private int current
= mainProgress
.getMin();
369 public void progress(Progress progress
, String name
) {
370 int diff
= progress
.getProgress() - current
;
376 StringBuilder builder
= new StringBuilder();
377 for (int i
= 0; i
< diff
; i
++) {
381 System
.err
.print(builder
.toString());
383 if (progress
.isDone()) {
384 System
.err
.println("");
388 Progress pg
= new Progress();
389 mainProgress
.addProgress(pg
, mainProgress
.getMax());
391 VersionCheck updates
= VersionCheck
.check();
392 if (updates
.isNewVersionAvailable()) {
393 // Sent to syserr so not to cause problem if one tries to capture a
394 // story content in text mode
396 .println("A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
397 System
.err
.println("");
398 for (Version v
: updates
.getNewer()) {
399 System
.err
.println("\tVersion " + v
);
400 System
.err
.println("\t-------------");
401 System
.err
.println("");
402 for (String it
: updates
.getChanges().get(v
)) {
403 System
.err
.println("\t- " + it
);
405 System
.err
.println("");
412 exitCode
= imprt(urlString
, pg
);
413 updates
.ok(); // we consider it read
416 exitCode
= export(luid
, sourceString
, target
, pg
);
417 updates
.ok(); // we consider it read
420 exitCode
= convert(urlString
, sourceString
, target
,
421 plusInfo
== null ?
false : plusInfo
, pg
);
422 updates
.ok(); // we consider it read
425 if (BasicReader
.getReader() == null) {
426 Instance
.getTraceHandler()
427 .error(new Exception(
428 "No reader type has been configured"));
432 exitCode
= list(sourceString
);
436 Instance
.getLibrary().changeSource(luid
, sourceString
, pg
);
437 } catch (IOException e1
) {
438 Instance
.getTraceHandler().error(e1
);
444 Instance
.getLibrary().changeTitle(luid
, titleString
, pg
);
445 } catch (IOException e1
) {
446 Instance
.getTraceHandler().error(e1
);
452 Instance
.getLibrary().changeAuthor(luid
, authorString
, pg
);
453 } catch (IOException e1
) {
454 Instance
.getTraceHandler().error(e1
);
459 if (BasicReader
.getReader() == null) {
460 Instance
.getTraceHandler()
461 .error(new Exception(
462 "No reader type has been configured"));
466 exitCode
= read(luid
, chapString
, true);
469 if (BasicReader
.getReader() == null) {
470 Instance
.getTraceHandler()
471 .error(new Exception(
472 "No reader type has been configured"));
476 exitCode
= read(urlString
, chapString
, false);
479 page
= page
== null ?
1 : page
;
481 Instance
.getTraceHandler().error("Incorrect page number");
486 item
= item
== null ?
0 : item
;
488 Instance
.getTraceHandler().error("Incorrect item number");
493 if (BasicReader
.getReader() == null) {
494 Instance
.getTraceHandler()
495 .error(new Exception(
496 "No reader type has been configured"));
502 if (searchOn
== null) {
503 BasicReader
.getReader().search(true);
504 } else if (search
!= null) {
506 BasicReader
.getReader().search(searchOn
, search
, page
,
511 } catch (IOException e1
) {
512 Instance
.getTraceHandler().error(e1
);
518 if (searchOn
== null) {
523 page
= page
== null ?
1 : page
;
525 Instance
.getTraceHandler().error("Incorrect page number");
530 item
= item
== null ?
0 : item
;
532 Instance
.getTraceHandler().error("Incorrect item number");
537 if (BasicReader
.getReader() == null) {
538 Instance
.getTraceHandler()
539 .error(new Exception(
540 "No reader type has been configured"));
546 BasicReader
.getReader().searchTag(searchOn
, page
, item
,
547 true, tags
.toArray(new Integer
[] {}));
548 } catch (IOException e1
) {
549 Instance
.getTraceHandler().error(e1
);
562 .println(String
.format("Fanfix version %s"
563 + "%nhttps://github.com/nikiroo/fanfix/"
564 + "%n\tWritten by Nikiroo",
565 Version
.getCurrentVersion()));
566 updates
.ok(); // we consider it read
569 if (BasicReader
.getReader() == null) {
570 Instance
.getTraceHandler()
571 .error(new Exception(
572 "No reader type has been configured"));
577 BasicReader
.getReader().browse(null);
578 } catch (IOException e
) {
579 Instance
.getTraceHandler().error(e
);
584 key
= Instance
.getConfig().getString(Config
.SERVER_KEY
);
585 port
= Instance
.getConfig().getInteger(Config
.SERVER_PORT
);
587 System
.err
.println("No port configured in the config file");
592 ServerObject server
= new RemoteLibraryServer(key
, port
);
593 server
.setTraceHandler(Instance
.getTraceHandler());
595 } catch (IOException e
) {
596 Instance
.getTraceHandler().error(e
);
600 // Can be given via "--remote XX XX XX"
602 key
= Instance
.getConfig().getString(Config
.SERVER_KEY
);
604 port
= Instance
.getConfig().getInteger(Config
.SERVER_PORT
);
607 System
.err
.println("No port given nor configured in the config file");
612 new RemoteLibrary(key
, host
, port
).exit();
613 } catch (SSLException e
) {
614 Instance
.getTraceHandler().error(
615 "Bad access key for remote library");
617 } catch (IOException e
) {
618 Instance
.getTraceHandler().error(e
);
624 exitCode
= 255; // should not be reachable (REMOTE -> START)
630 Instance
.getTempFiles().close();
631 } catch (IOException e
) {
632 Instance
.getTraceHandler()
633 .error(new IOException(
634 "Cannot dispose of the temporary files", e
));
637 if (exitCode
== 255) {
641 System
.exit(exitCode
);
645 * Import the given resource into the {@link LocalLibrary}.
648 * the resource to import
650 * the optional progress reporter
652 * @return the exit return code (0 = success)
654 public static int imprt(String urlString
, Progress pg
) {
656 MetaData meta
= Instance
.getLibrary().imprt(
657 BasicReader
.getUrl(urlString
), pg
);
658 System
.out
.println(meta
.getLuid() + ": \"" + meta
.getTitle()
660 } catch (IOException e
) {
661 Instance
.getTraceHandler().error(e
);
669 * Export the {@link Story} from the {@link LocalLibrary} to the given
675 * the {@link OutputType} to use
679 * the optional progress reporter
681 * @return the exit return code (0 = success)
683 public static int export(String luid
, String typeString
, String target
,
685 OutputType type
= OutputType
.valueOfNullOkUC(typeString
, null);
687 Instance
.getTraceHandler().error(
688 new Exception(trans(StringId
.OUTPUT_DESC
, typeString
)));
693 Instance
.getLibrary().export(luid
, type
, target
, pg
);
694 } catch (IOException e
) {
695 Instance
.getTraceHandler().error(e
);
703 * List the stories of the given source from the {@link LocalLibrary}
704 * (unless NULL is passed, in which case all stories will be listed).
707 * the source to list the known stories of, or NULL to list all
710 * @return the exit return code (0 = success)
712 private static int list(String source
) {
713 BasicReader
.setDefaultReaderType(ReaderType
.CLI
);
715 BasicReader
.getReader().browse(source
);
716 } catch (IOException e
) {
717 Instance
.getTraceHandler().error(e
);
725 * Start the current reader for this {@link Story}.
728 * the LUID of the {@link Story} in the {@link LocalLibrary}
729 * <b>or</b> the {@link Story} {@link URL}
731 * which {@link Chapter} to read (starting at 1), or NULL to get
732 * the {@link Story} description
734 * TRUE if the source is the {@link Story} LUID, FALSE if it is a
737 * @return the exit return code (0 = success)
739 private static int read(String story
, String chapString
, boolean library
) {
741 Reader reader
= BasicReader
.getReader();
743 reader
.setMeta(story
);
745 reader
.setMeta(BasicReader
.getUrl(story
), null);
748 if (chapString
!= null) {
750 reader
.setChapter(Integer
.parseInt(chapString
));
752 } catch (NumberFormatException e
) {
753 Instance
.getTraceHandler().error(
754 new IOException("Chapter number cannot be parsed: "
761 } catch (IOException e
) {
762 Instance
.getTraceHandler().error(e
);
770 * Convert the {@link Story} into another format.
773 * the source {@link Story} to convert
775 * the {@link OutputType} to convert to
779 * TRUE to also export the cover and info file, even if the given
780 * {@link OutputType} does not usually save them
782 * the optional progress reporter
784 * @return the exit return code (0 = success)
786 public static int convert(String urlString
, String typeString
,
787 String target
, boolean infoCover
, Progress pg
) {
790 Instance
.getTraceHandler().trace("Convert: " + urlString
);
791 String sourceName
= urlString
;
793 URL source
= BasicReader
.getUrl(urlString
);
794 sourceName
= source
.toString();
795 if (sourceName
.startsWith("file://")) {
796 sourceName
= sourceName
.substring("file://".length());
799 OutputType type
= OutputType
.valueOfAllOkUC(typeString
, null);
801 Instance
.getTraceHandler().error(
802 new IOException(trans(StringId
.ERR_BAD_OUTPUT_TYPE
,
808 BasicSupport support
= BasicSupport
.getSupport(source
);
810 if (support
!= null) {
811 Instance
.getTraceHandler().trace(
812 "Support found: " + support
.getClass());
813 Progress pgIn
= new Progress();
814 Progress pgOut
= new Progress();
817 pg
.addProgress(pgIn
, 1);
818 pg
.addProgress(pgOut
, 1);
821 Story story
= support
.process(pgIn
);
823 target
= new File(target
).getAbsolutePath();
824 BasicOutput
.getOutput(type
, infoCover
, infoCover
)
825 .process(story
, target
, pgOut
);
826 } catch (IOException e
) {
827 Instance
.getTraceHandler().error(
828 new IOException(trans(StringId
.ERR_SAVING
,
833 Instance
.getTraceHandler().error(
834 new IOException(trans(
835 StringId
.ERR_NOT_SUPPORTED
, source
)));
839 } catch (IOException e
) {
840 Instance
.getTraceHandler().error(
841 new IOException(trans(StringId
.ERR_LOADING
,
846 } catch (MalformedURLException e
) {
847 Instance
.getTraceHandler()
848 .error(new IOException(trans(StringId
.ERR_BAD_URL
,
857 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
860 * the ID to translate
862 * @return the translated result
864 private static String
trans(StringId id
, Object
... params
) {
865 return Instance
.getTrans().getString(id
, params
);
869 * Display the correct syntax of the program to the user to stdout, or an
870 * error message if the syntax used was wrong on stderr.
873 * TRUE to show the syntax help, FALSE to show "syntax error"
875 private static void syntax(boolean showHelp
) {
877 StringBuilder builder
= new StringBuilder();
878 for (SupportType type
: SupportType
.values()) {
879 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
881 builder
.append('\n');
884 String typesIn
= builder
.toString();
885 builder
.setLength(0);
887 for (OutputType type
: OutputType
.values()) {
888 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
889 type
.getDesc(true)));
890 builder
.append('\n');
893 String typesOut
= builder
.toString();
895 System
.out
.println(trans(StringId
.HELP_SYNTAX
, typesIn
, typesOut
));
897 System
.err
.println(trans(StringId
.ERR_SYNTAX
));
902 * Set the default reader type for this session only (it can be changed in
903 * the configuration file, too, but this value will override it).
905 * @param readerTypeString
908 private static int setReaderType(String readerTypeString
) {
910 ReaderType readerType
= ReaderType
.valueOf(readerTypeString
912 BasicReader
.setDefaultReaderType(readerType
);
914 } catch (IllegalArgumentException e
) {
915 Instance
.getTraceHandler().error(
916 new IOException("Unknown reader type: " + readerTypeString
,