1 package be
.nikiroo
.fanfix
;
4 import java
.io
.IOException
;
5 import java
.net
.MalformedURLException
;
7 import java
.util
.ArrayList
;
10 import be
.nikiroo
.fanfix
.bundles
.StringId
;
11 import be
.nikiroo
.fanfix
.data
.Chapter
;
12 import be
.nikiroo
.fanfix
.data
.MetaData
;
13 import be
.nikiroo
.fanfix
.data
.Story
;
14 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
15 import be
.nikiroo
.fanfix
.library
.CacheLibrary
;
16 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
17 import be
.nikiroo
.fanfix
.library
.RemoteLibrary
;
18 import be
.nikiroo
.fanfix
.library
.RemoteLibraryServer
;
19 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
20 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
21 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
22 import be
.nikiroo
.fanfix
.reader
.Reader
;
23 import be
.nikiroo
.fanfix
.reader
.Reader
.ReaderType
;
24 import be
.nikiroo
.fanfix
.searchable
.BasicSearchable
;
25 import be
.nikiroo
.fanfix
.supported
.BasicSupport
;
26 import be
.nikiroo
.fanfix
.supported
.SupportType
;
27 import be
.nikiroo
.utils
.Progress
;
28 import be
.nikiroo
.utils
.Version
;
29 import be
.nikiroo
.utils
.serial
.server
.ServerObject
;
32 * Main program entry point.
37 private enum MainAction
{
38 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
42 * Main program entry point.
44 * Known environment variables:
46 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
47 * {@link String}s when possible</li>
48 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
49 * before taking the usual ones; they will also be saved/updated into this
50 * path when the program starts</li>
51 * <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
52 * configuration value with 'true'</li>
56 * <li>--import [URL]: import into library</li>
57 * <li>--export [id] [output_type] [target]: export story to target</li>
58 * <li>--convert [URL] [output_type] [target] (+info): convert URL into
60 * <li>--read [id] ([chapter number]): read the given story from the library
62 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
63 * story, without saving it</li>
64 * <li>--search WEBSITE [free text] ([page] ([item])): search for the given terms,
65 * show the given page (page 0 means "how many page do we have", starts at page 1)</li>
66 * <li>--search-tag WEBSITE ([tag 1] [tag2...] ([page] ([item]))): list the known
67 * tags or search the stories for the given tag(s), show the given page of results
68 * (page 0 means "how many page do we have", starts at page 1)</li>
69 * <li>--list ([type]): list the stories present in the library</li>
70 * <li>--set-source [id] [new source]: change the source of the given story</li>
71 * <li>--set-title [id] [new title]: change the title of the given story</li>
72 * <li>--set-author [id] [new author]: change the author of the given story</li>
73 * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
74 * for this command</li>
75 * <li>--version: get the version of the program</li>
76 * <li>--server [key] [port]: start a server on this port</li>
77 * <li>--stop-server [key] [port]: stop the running server on this port if
79 * <li>--remote [key] [host] [port]: use a the given remote library</li>
83 * see method description
85 public static void main(String
[] args
) {
86 String urlString
= null;
88 String sourceString
= null;
89 String titleString
= null;
90 String authorString
= null;
91 String chapString
= null;
94 MainAction action
= MainAction
.START
;
95 Boolean plusInfo
= null;
98 SupportType searchOn
= null;
100 List
<String
> tags
= new ArrayList
<String
>();
104 boolean noMoreActions
= false;
107 for (int i
= 0; exitCode
== 0 && i
< args
.length
; i
++) {
108 // Action (--) handling:
109 if (!noMoreActions
&& args
[i
].startsWith("--")) {
110 if (args
[i
].equals("--")) {
111 noMoreActions
= true;
114 action
= MainAction
.valueOf(args
[i
].substring(2)
115 .toUpperCase().replace("-", "_"));
116 } catch (Exception e
) {
117 Instance
.getTraceHandler().error(
118 new IllegalArgumentException("Unknown action: "
129 if (urlString
== null) {
138 } else if (sourceString
== null) {
139 sourceString
= args
[i
];
140 } else if (target
== null) {
147 if (urlString
== null) {
149 } else if (sourceString
== null) {
150 sourceString
= args
[i
];
151 } else if (target
== null) {
153 } else if (plusInfo
== null) {
154 if ("+info".equals(args
[i
])) {
164 if (sourceString
== null) {
165 sourceString
= args
[i
];
173 } else if (sourceString
== null) {
174 sourceString
= args
[i
];
182 } else if (sourceString
== null) {
183 titleString
= args
[i
];
191 } else if (sourceString
== null) {
192 authorString
= args
[i
];
200 } else if (chapString
== null) {
201 chapString
= args
[i
];
207 if (urlString
== null) {
209 } else if (chapString
== null) {
210 chapString
= args
[i
];
216 if (searchOn
== null) {
217 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
219 if (searchOn
== null) {
220 Instance
.getTraceHandler().error(
221 "Website not known: <" + args
[i
] + ">");
225 if (BasicSearchable
.getSearchable(searchOn
) == null) {
226 Instance
.getTraceHandler().error(
227 "Website not supported: " + searchOn
);
230 } else if (search
== null) {
232 } else if (page
== null) {
234 page
= Integer
.parseInt(args
[i
]);
235 } catch (NumberFormatException e
) {
236 Instance
.getTraceHandler().error(
237 new Exception("Incorrect page number: <"
238 + args
[i
] + ">", e
));
241 } else if (item
== null) {
243 item
= Integer
.parseInt(args
[i
]);
244 } catch (NumberFormatException e
) {
245 Instance
.getTraceHandler().error(
246 new Exception("Incorrect item number: <"
247 + args
[i
] + ">", e
));
255 if (searchOn
== null) {
256 searchOn
= SupportType
.valueOfAllOkUC(args
[i
]);
258 if (searchOn
== null) {
259 Instance
.getTraceHandler().error(
260 "Website not known: <" + args
[i
] + ">");
264 if (BasicSearchable
.getSearchable(searchOn
) == null) {
265 Instance
.getTraceHandler().error(
266 "Website not supported: " + searchOn
);
277 exitCode
= setReaderType(args
[i
]);
278 action
= MainAction
.START
;
281 exitCode
= 255; // not supposed to be selected by user
284 exitCode
= 255; // no arguments for this option
290 } else if (port
== null) {
291 port
= Integer
.parseInt(args
[i
]);
299 } else if (host
== null) {
301 } else if (port
== null) {
302 port
= Integer
.parseInt(args
[i
]);
304 BasicLibrary lib
= new RemoteLibrary(key
, host
, port
);
305 lib
= new CacheLibrary(Instance
.getRemoteDir(host
), lib
);
307 BasicReader
.setDefaultLibrary(lib
);
309 action
= MainAction
.START
;
317 final Progress mainProgress
= new Progress(0, 80);
318 mainProgress
.addProgressListener(new Progress
.ProgressListener() {
319 private int current
= mainProgress
.getMin();
322 public void progress(Progress progress
, String name
) {
323 int diff
= progress
.getProgress() - current
;
329 StringBuilder builder
= new StringBuilder();
330 for (int i
= 0; i
< diff
; i
++) {
334 System
.err
.print(builder
.toString());
336 if (progress
.isDone()) {
337 System
.err
.println("");
341 Progress pg
= new Progress();
342 mainProgress
.addProgress(pg
, mainProgress
.getMax());
344 VersionCheck updates
= VersionCheck
.check();
345 if (updates
.isNewVersionAvailable()) {
346 // Sent to syserr so not to cause problem if one tries to capture a
347 // story content in text mode
349 .println("A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
350 System
.err
.println("");
351 for (Version v
: updates
.getNewer()) {
352 System
.err
.println("\tVersion " + v
);
353 System
.err
.println("\t-------------");
354 System
.err
.println("");
355 for (String it
: updates
.getChanges().get(v
)) {
356 System
.err
.println("\t- " + it
);
358 System
.err
.println("");
362 if (exitCode
!= 255) {
365 exitCode
= imprt(urlString
, pg
);
366 updates
.ok(); // we consider it read
369 exitCode
= export(luid
, sourceString
, target
, pg
);
370 updates
.ok(); // we consider it read
373 exitCode
= convert(urlString
, sourceString
, target
,
374 plusInfo
== null ?
false : plusInfo
, pg
);
375 updates
.ok(); // we consider it read
378 if (BasicReader
.getReader() == null) {
379 Instance
.getTraceHandler()
380 .error(new Exception(
381 "No reader type has been configured"));
385 exitCode
= list(sourceString
);
389 Instance
.getLibrary().changeSource(luid
, sourceString
, pg
);
390 } catch (IOException e1
) {
391 Instance
.getTraceHandler().error(e1
);
397 Instance
.getLibrary().changeTitle(luid
, titleString
, pg
);
398 } catch (IOException e1
) {
399 Instance
.getTraceHandler().error(e1
);
405 Instance
.getLibrary().changeAuthor(luid
, authorString
, pg
);
406 } catch (IOException e1
) {
407 Instance
.getTraceHandler().error(e1
);
412 if (BasicReader
.getReader() == null) {
413 Instance
.getTraceHandler()
414 .error(new Exception(
415 "No reader type has been configured"));
419 exitCode
= read(luid
, chapString
, true);
422 if (BasicReader
.getReader() == null) {
423 Instance
.getTraceHandler()
424 .error(new Exception(
425 "No reader type has been configured"));
429 exitCode
= read(urlString
, chapString
, false);
432 if (searchOn
== null || search
== null) {
444 if (BasicReader
.getReader() == null) {
445 Instance
.getTraceHandler()
446 .error(new Exception(
447 "No reader type has been configured"));
453 BasicReader
.getReader().search(searchOn
, search
, page
, item
);
454 } catch (IOException e1
) {
455 Instance
.getTraceHandler().error(e1
);
460 if (searchOn
== null) {
469 page
= Integer
.parseInt(tags
.get(tags
.size()-1));
470 tags
.remove(tags
.size() - 1);
473 int tmp
= Integer
.parseInt(tags
.get(tags
.size()-1));
474 tags
.remove(tags
.size() - 1);
478 } catch (Exception e
) {
480 } catch (Exception e
) {
483 if (BasicReader
.getReader() == null) {
484 Instance
.getTraceHandler()
485 .error(new Exception(
486 "No reader type has been configured"));
492 BasicReader
.getReader().searchTag(searchOn
, page
, item
, tags
.toArray(new String
[]{}));
493 } catch (IOException e1
) {
494 Instance
.getTraceHandler().error(e1
);
507 .println(String
.format("Fanfix version %s"
508 + "%nhttps://github.com/nikiroo/fanfix/"
509 + "%n\tWritten by Nikiroo",
510 Version
.getCurrentVersion()));
511 updates
.ok(); // we consider it read
514 if (BasicReader
.getReader() == null) {
515 Instance
.getTraceHandler()
516 .error(new Exception(
517 "No reader type has been configured"));
521 BasicReader
.getReader().browse(null);
529 ServerObject server
= new RemoteLibraryServer(key
, port
);
530 server
.setTraceHandler(Instance
.getTraceHandler());
532 } catch (IOException e
) {
533 Instance
.getTraceHandler().error(e
);
542 new RemoteLibrary(key
, host
, port
).exit();
545 exitCode
= 255; // should not be reachable (REMOTE -> START)
551 Instance
.getTempFiles().close();
552 } catch (IOException e
) {
553 Instance
.getTraceHandler()
554 .error(new IOException(
555 "Cannot dispose of the temporary files", e
));
558 if (exitCode
== 255) {
562 System
.exit(exitCode
);
566 * Import the given resource into the {@link LocalLibrary}.
569 * the resource to import
571 * the optional progress reporter
573 * @return the exit return code (0 = success)
575 public static int imprt(String urlString
, Progress pg
) {
577 Story story
= Instance
.getLibrary().imprt(
578 BasicReader
.getUrl(urlString
), pg
);
579 System
.out
.println(story
.getMeta().getLuid() + ": \""
580 + story
.getMeta().getTitle() + "\" imported.");
581 } catch (IOException e
) {
582 Instance
.getTraceHandler().error(e
);
590 * Export the {@link Story} from the {@link LocalLibrary} to the given
596 * the {@link OutputType} to use
600 * the optional progress reporter
602 * @return the exit return code (0 = success)
604 public static int export(String luid
, String typeString
, String target
,
606 OutputType type
= OutputType
.valueOfNullOkUC(typeString
, null);
608 Instance
.getTraceHandler().error(
609 new Exception(trans(StringId
.OUTPUT_DESC
, typeString
)));
614 Instance
.getLibrary().export(luid
, type
, target
, pg
);
615 } catch (IOException e
) {
616 Instance
.getTraceHandler().error(e
);
624 * List the stories of the given source from the {@link LocalLibrary}
625 * (unless NULL is passed, in which case all stories will be listed).
628 * the source to list the known stories of, or NULL to list all
631 * @return the exit return code (0 = success)
633 private static int list(String source
) {
634 List
<MetaData
> stories
;
635 stories
= BasicReader
.getReader().getLibrary().getListBySource(source
);
637 for (MetaData story
: stories
) {
639 if (story
.getAuthor() != null && !story
.getAuthor().isEmpty()) {
640 author
= " (" + story
.getAuthor() + ")";
643 System
.out
.println(story
.getLuid() + ": " + story
.getTitle()
650 * Start the current reader for this {@link Story}.
653 * the LUID of the {@link Story} in the {@link LocalLibrary}
654 * <b>or</b> the {@link Story} {@link URL}
656 * which {@link Chapter} to read (starting at 1), or NULL to get
657 * the {@link Story} description
659 * TRUE if the source is the {@link Story} LUID, FALSE if it is a
662 * @return the exit return code (0 = success)
664 private static int read(String story
, String chapString
, boolean library
) {
666 Reader reader
= BasicReader
.getReader();
668 reader
.setMeta(story
);
670 reader
.setMeta(BasicReader
.getUrl(story
), null);
673 if (chapString
!= null) {
675 reader
.setChapter(Integer
.parseInt(chapString
));
677 } catch (NumberFormatException e
) {
678 Instance
.getTraceHandler().error(
679 new IOException("Chapter number cannot be parsed: "
686 } catch (IOException e
) {
687 Instance
.getTraceHandler().error(e
);
695 * Convert the {@link Story} into another format.
698 * the source {@link Story} to convert
700 * the {@link OutputType} to convert to
704 * TRUE to also export the cover and info file, even if the given
705 * {@link OutputType} does not usually save them
707 * the optional progress reporter
709 * @return the exit return code (0 = success)
711 public static int convert(String urlString
, String typeString
,
712 String target
, boolean infoCover
, Progress pg
) {
715 Instance
.getTraceHandler().trace("Convert: " + urlString
);
716 String sourceName
= urlString
;
718 URL source
= BasicReader
.getUrl(urlString
);
719 sourceName
= source
.toString();
720 if (source
.toString().startsWith("file://")) {
721 sourceName
= sourceName
.substring("file://".length());
724 OutputType type
= OutputType
.valueOfAllOkUC(typeString
, null);
726 Instance
.getTraceHandler().error(
727 new IOException(trans(StringId
.ERR_BAD_OUTPUT_TYPE
,
733 BasicSupport support
= BasicSupport
.getSupport(source
);
735 if (support
!= null) {
736 Instance
.getTraceHandler().trace(
737 "Support found: " + support
.getClass());
738 Progress pgIn
= new Progress();
739 Progress pgOut
= new Progress();
742 pg
.addProgress(pgIn
, 1);
743 pg
.addProgress(pgOut
, 1);
746 Story story
= support
.process(pgIn
);
748 target
= new File(target
).getAbsolutePath();
749 BasicOutput
.getOutput(type
, infoCover
, infoCover
)
750 .process(story
, target
, pgOut
);
751 } catch (IOException e
) {
752 Instance
.getTraceHandler().error(
753 new IOException(trans(StringId
.ERR_SAVING
,
758 Instance
.getTraceHandler().error(
759 new IOException(trans(
760 StringId
.ERR_NOT_SUPPORTED
, source
)));
764 } catch (IOException e
) {
765 Instance
.getTraceHandler().error(
766 new IOException(trans(StringId
.ERR_LOADING
,
771 } catch (MalformedURLException e
) {
772 Instance
.getTraceHandler()
773 .error(new IOException(trans(StringId
.ERR_BAD_URL
,
782 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
785 * the ID to translate
787 * @return the translated result
789 private static String
trans(StringId id
, Object
... params
) {
790 return Instance
.getTrans().getString(id
, params
);
794 * Display the correct syntax of the program to the user to stdout, or an
795 * error message if the syntax used was wrong on stderr.
798 * TRUE to show the syntax help, FALSE to show "syntax error"
800 private static void syntax(boolean showHelp
) {
802 StringBuilder builder
= new StringBuilder();
803 for (SupportType type
: SupportType
.values()) {
804 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
806 builder
.append('\n');
809 String typesIn
= builder
.toString();
810 builder
.setLength(0);
812 for (OutputType type
: OutputType
.values()) {
813 builder
.append(trans(StringId
.ERR_SYNTAX_TYPE
, type
.toString(),
814 type
.getDesc(true)));
815 builder
.append('\n');
818 String typesOut
= builder
.toString();
820 System
.out
.println(trans(StringId
.HELP_SYNTAX
, typesIn
, typesOut
));
822 System
.err
.println(trans(StringId
.ERR_SYNTAX
));
827 * Set the default reader type for this session only (it can be changed in
828 * the configuration file, too, but this value will override it).
830 * @param readerTypeString
833 private static int setReaderType(String readerTypeString
) {
835 ReaderType readerType
= ReaderType
.valueOf(readerTypeString
837 BasicReader
.setDefaultReaderType(readerType
);
839 } catch (IllegalArgumentException e
) {
840 Instance
.getTraceHandler().error(
841 new IOException("Unknown reader type: " + readerTypeString
,