From 08fe2e33007063e30fe22dc1d290f8afaa18eb1d Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Sat, 11 Feb 2017 22:24:09 +0100 Subject: [PATCH] Initial commit (working) This is not the actual first commit (obviously), but the first commit since it was exported to GitHub. It works correctly as of today, but I still have a few websites I want to add to replace my old BASH scripts. --- Makefile.base | 136 ++ README.md | 73 +- configure.sh | 42 + libs/nikiroo-utils-0.9.1-sources.jar | Bin 0 -> 16886 bytes libs/unbescape-1.1.4-sources.jar | Bin 0 -> 184047 bytes libs/unbescape-1.1.4_ChangeLog.txt | 32 + libs/unbescape-1.1.4_LICENSE.txt | 202 +++ src/.gitattributes | 49 + src/.gitignore | 8 + src/be/nikiroo/fanfix/Cache.java | 427 ++++++ src/be/nikiroo/fanfix/Instance.java | 195 +++ src/be/nikiroo/fanfix/Library.java | 261 ++++ src/be/nikiroo/fanfix/Main.java | 341 +++++ src/be/nikiroo/fanfix/bundles/Config.java | 45 + .../nikiroo/fanfix/bundles/ConfigBundle.java | 38 + src/be/nikiroo/fanfix/bundles/StringId.java | 122 ++ .../fanfix/bundles/StringIdBundle.java | 40 + src/be/nikiroo/fanfix/bundles/Target.java | 19 + .../nikiroo/fanfix/bundles/config.properties | 55 + .../nikiroo/fanfix/bundles/package-info.java | 8 + .../fanfix/bundles/resources.properties | 134 ++ src/be/nikiroo/fanfix/data/Chapter.java | 102 ++ src/be/nikiroo/fanfix/data/MetaData.java | 276 ++++ src/be/nikiroo/fanfix/data/Paragraph.java | 104 ++ src/be/nikiroo/fanfix/data/Story.java | 103 ++ src/be/nikiroo/fanfix/data/package-info.java | 7 + src/be/nikiroo/fanfix/output/BasicOutput.java | 427 ++++++ src/be/nikiroo/fanfix/output/Cbz.java | 88 ++ src/be/nikiroo/fanfix/output/Epub.java | 471 ++++++ src/be/nikiroo/fanfix/output/InfoCover.java | 86 ++ src/be/nikiroo/fanfix/output/InfoText.java | 74 + src/be/nikiroo/fanfix/output/LaTeX.java | 182 +++ src/be/nikiroo/fanfix/output/Sysout.java | 22 + src/be/nikiroo/fanfix/output/Text.java | 126 ++ src/be/nikiroo/fanfix/output/epub.style.css | 103 ++ .../nikiroo/fanfix/output/package-info.java | 12 + src/be/nikiroo/fanfix/package-info.java | 10 + src/be/nikiroo/fanfix/reader/CliReader.java | 146 ++ .../fanfix/supported/BasicSupport.java | 1292 +++++++++++++++++ src/be/nikiroo/fanfix/supported/Cbz.java | 93 ++ src/be/nikiroo/fanfix/supported/E621.java | 257 ++++ src/be/nikiroo/fanfix/supported/Epub.java | 293 ++++ .../nikiroo/fanfix/supported/Fanfiction.java | 289 ++++ .../nikiroo/fanfix/supported/Fimfiction.java | 236 +++ src/be/nikiroo/fanfix/supported/InfoText.java | 248 ++++ src/be/nikiroo/fanfix/supported/MangaFox.java | 409 ++++++ src/be/nikiroo/fanfix/supported/Text.java | 295 ++++ .../fanfix/supported/package-info.java | 11 + 48 files changed, 7987 insertions(+), 2 deletions(-) create mode 100644 Makefile.base create mode 100755 configure.sh create mode 100644 libs/nikiroo-utils-0.9.1-sources.jar create mode 100644 libs/unbescape-1.1.4-sources.jar create mode 100644 libs/unbescape-1.1.4_ChangeLog.txt create mode 100644 libs/unbescape-1.1.4_LICENSE.txt create mode 100644 src/.gitattributes create mode 100644 src/.gitignore create mode 100644 src/be/nikiroo/fanfix/Cache.java create mode 100644 src/be/nikiroo/fanfix/Instance.java create mode 100644 src/be/nikiroo/fanfix/Library.java create mode 100644 src/be/nikiroo/fanfix/Main.java create mode 100644 src/be/nikiroo/fanfix/bundles/Config.java create mode 100644 src/be/nikiroo/fanfix/bundles/ConfigBundle.java create mode 100644 src/be/nikiroo/fanfix/bundles/StringId.java create mode 100644 src/be/nikiroo/fanfix/bundles/StringIdBundle.java create mode 100644 src/be/nikiroo/fanfix/bundles/Target.java create mode 100644 src/be/nikiroo/fanfix/bundles/config.properties create mode 100644 src/be/nikiroo/fanfix/bundles/package-info.java create mode 100644 src/be/nikiroo/fanfix/bundles/resources.properties create mode 100644 src/be/nikiroo/fanfix/data/Chapter.java create mode 100644 src/be/nikiroo/fanfix/data/MetaData.java create mode 100644 src/be/nikiroo/fanfix/data/Paragraph.java create mode 100644 src/be/nikiroo/fanfix/data/Story.java create mode 100644 src/be/nikiroo/fanfix/data/package-info.java create mode 100644 src/be/nikiroo/fanfix/output/BasicOutput.java create mode 100644 src/be/nikiroo/fanfix/output/Cbz.java create mode 100644 src/be/nikiroo/fanfix/output/Epub.java create mode 100644 src/be/nikiroo/fanfix/output/InfoCover.java create mode 100644 src/be/nikiroo/fanfix/output/InfoText.java create mode 100644 src/be/nikiroo/fanfix/output/LaTeX.java create mode 100644 src/be/nikiroo/fanfix/output/Sysout.java create mode 100644 src/be/nikiroo/fanfix/output/Text.java create mode 100644 src/be/nikiroo/fanfix/output/epub.style.css create mode 100644 src/be/nikiroo/fanfix/output/package-info.java create mode 100644 src/be/nikiroo/fanfix/package-info.java create mode 100644 src/be/nikiroo/fanfix/reader/CliReader.java create mode 100644 src/be/nikiroo/fanfix/supported/BasicSupport.java create mode 100644 src/be/nikiroo/fanfix/supported/Cbz.java create mode 100644 src/be/nikiroo/fanfix/supported/E621.java create mode 100644 src/be/nikiroo/fanfix/supported/Epub.java create mode 100644 src/be/nikiroo/fanfix/supported/Fanfiction.java create mode 100644 src/be/nikiroo/fanfix/supported/Fimfiction.java create mode 100644 src/be/nikiroo/fanfix/supported/InfoText.java create mode 100644 src/be/nikiroo/fanfix/supported/MangaFox.java create mode 100644 src/be/nikiroo/fanfix/supported/Text.java create mode 100644 src/be/nikiroo/fanfix/supported/package-info.java diff --git a/Makefile.base b/Makefile.base new file mode 100644 index 00000000..300db504 --- /dev/null +++ b/Makefile.base @@ -0,0 +1,136 @@ +# Required parameters (the commented out ones are supposed to change per project): + +#MAIN = path to main java source to compile +#MORE = path to supplementary needed resources not linked from MAIN +#NAME = name of project (used for jar output file) +#PREFIX = usually /usr/local (where to install the program) +#TEST = path to main test source to compile +#JAR_FLAGS += a list of things to pack, each usually prefixed with "-C bin/" + +JAVAC = javac +JAVAC_FLAGS += -encoding UTF-8 -d ./bin/ -cp ./src/ -Xdiags:verbose +JAVA = java +JAVA_FLAGS += -cp ./bin/ +JAR = jar +RJAR = java +RJAR_FLAGS += -jar + +# Usual options: +# make : to build the jar file +# make libs : to update the libraries into src/ +# make build : to update the binaries (not the jar) +# make test : to update the test binaries +# make build jar : to update the binaries and jar file +# make clean : to clean the directory of intermediate files +# make mrpropre : to clean the directory of all outputs +# make run : to run the program from the binaries +# make run-test : to run the test program from the binaries +# make jrun : to run the program from the jar file +# make install : to install the application into $PREFIX + +# Note: build is actually slower than rebuild in most cases except when +# small changes only are detected ; so we use rebuild by default + +all: build jar + +.PHONY: all clean mrproper mrpropre build run jrun jar resources install libs love + +bin: + @mkdir -p bin + +jar: $(NAME).jar + +build: resources + @echo Compiling program... + @echo " src/$(MAIN)" + @$(JAVAC) $(JAVAC_FLAGS) "src/$(MAIN).java" + @[ "$(MORE)" = "" ] || for sup in $(MORE); do \ + echo " src/$$sup" ;\ + $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \ + done + +test: + @[ -e bin/$(MAIN).class ] || echo You need to build the sources + @[ -e bin/$(MAIN).class ] + @echo Compiling test program... + @[ "$(TEST)" != "" ] || echo No test sources defined. + @[ "$(TEST)" = "" ] || for sup in $(TEST); do \ + echo " src/$$sup" ;\ + $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \ + done + +clean: + rm -rf bin/ + @echo Removing sources taken from libs... + @for lib in libs/*.jar; do \ + basename "$$lib"; \ + jar tf "$$lib" | while read -r ln; do \ + [ -f "src/$$ln" ] && rm "src/$$ln"; \ + done; \ + jar tf "$$lib" | tac | while read -r ln; do \ + [ -d "src/$$ln" ] && rmdir "src/$$ln" 2>/dev/null || true; \ + done; \ + done + +mrproper: mrpropre + +mrpropre: clean + rm -f $(NAME).jar + +love: + @echo " ...not war." + +resources: libs + @echo Copying resources into bin/... + @cd src && find . | grep -v '\.java$$' | while read -r ln; do \ + if [ -f "$$ln" ]; then \ + dir="`dirname "$$ln"`"; \ + mkdir -p "../bin/$$dir" ; \ + cp "$$ln" "../bin/$$ln" ; \ + fi ; \ + done + +libs: bin + @[ -e bin/libs -o ! -d libs ] || echo Extracting sources from libs... + @[ -e bin/libs -o ! -d libs ] || (cd src&& for lib in ../libs/*.jar;do \ + basename "$$lib"; \ + jar xf "$$lib"; \ + done ) + @[ ! -d libs ] || touch bin/libs + +$(NAME).jar: resources + @[ -e bin/$(MAIN).class ] || echo You need to build the sources + @[ -e bin/$(MAIN).class ] + @echo Making JAR file... + @echo "Main-Class: `echo "$(MAIN)" | sed 's:/:.:g'`" > bin/manifest + @echo >> bin/manifest + $(JAR) cfm $(NAME).jar bin/manifest $(JAR_FLAGS) + +run: + @[ -e bin/$(MAIN).class ] || echo You need to build the sources + @[ -e bin/$(MAIN).class ] + @echo Running "$(NAME)"... + $(JAVA) $(JAVA_FLAGS) $(MAIN) + +jrun: + @[ -e $(NAME).jar ] || echo You need to build the jar + @[ -e $(NAME).jar ] + @echo Running "$(NAME).jar"... + $(RJAR) $(RJAR_FLAGS) $(NAME).jar + +run-test: + @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ] || echo You need to build the test sources + @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ] + @echo Running tests for "$(NAME)"... + @[ "$(TEST)" != "" ] || echo No test sources defined. + [ "$(TEST)" = "" ] || $(JAVA) $(JAVA_FLAGS) $(TEST) + +install: + @[ -e $(NAME).jar ] || echo You need to build the jar + @[ -e $(NAME).jar ] + mkdir -p "$(PREFIX)/lib" "$(PREFIX)/bin" + cp $(NAME).jar "$(PREFIX)/lib/" + echo "#!/bin/sh" > "$(PREFIX)/bin/$(NAME)" + echo "$(RJAR) $(RJAR_FLAGS) \"$(PREFIX)/lib/$(NAME).jar\" \"$$@\"" >> "$(PREFIX)/bin/$(NAME)" + chmod a+rx "$(PREFIX)/bin/$(NAME)" + diff --git a/README.md b/README.md index 83ca0813..39f7fb43 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# fanfix -A small program to download and convert fanfictions and comics from supported websites into offline files (epu, cbz...) +# Fanfix + +A small program to download and convert fanfictions and comics from supported websites into offline files (epu, cbz...). + +It can either "convert" a website into a file, or import/export to/from its library. + +## Supported platforms + +Any platform with at lest Java 1.5 on it should be ok. + +It was only tested on Linux up until now, though. + +If you have any problems to compile it on another platform or with a supported Java version (1.4 won't work, but you may try to cross-compile; 1.8 had been tested and works), please contact me. + +## Usage + +- java -jar fanfix.jar --convert http://SOME_SUPPORTE_URL/ epub /home/niki/my-story.epub: will convert the story to EPUB +- java -jar fanfix.jar --import http://SOME_SUPPORTED_URL/ : will import the story into the local library +- java -jar fanfix.jar --export LUID CBZ /tmp/comix.cbz : will export the story from the local library +- java -jar fanfix.jar --list: will list the known stories and their LUIDs from the local library +- ... (calling the program without parameters will display the syntax) + +### Environment variables + +- LANG=en java -jar fanfix.jar: force the language to English (the only one for now...) +- CONFIG_DIR=$HOME/.fanfix java -jar fanfix.jar: use the given directory as a config directory (and copy the default configuration if needed) +- NOUTF=1 java -jar fanfix.jar: try to fallback to non-unicode values when possible (can have an impact on the resulting files, not only on user messages) + +## Compilation + +./configure.sh && make + +You can also import the java sources into, say, Eclipse, and create a runnable JAR file from there. +Just remember to unpak the 2 dependant libraries before (or "make libs" can do it). + +### Dependant libraries (included) + +- libs/nikiroo-utils-sources-0.9.1.jar: some shared utility functions I also use elsewhere +- libs/unbescape-1.1.4-sources.jar: a nice library to escape/unescape a lot of text formats; I only use it for HTML + +Nothing else but Java 1.5+. + +Note that calling "make libs" will export the libraries into the src/ directory. + +## Supported websites + +Currently, the following websites are supported: +- http://FimFiction.net/: Fanfictions devoted to the My Little Pony show +- http://Fanfiction.net/: Fan fictions of many, many different universes, from TV shows to novels to games. +- http://mangafox.me/: A well filled repository of mangas, or, as their website states: Most popular manga scanlations read online for free at mangafox, as well as a close-knit community to chat and make friends. +- https://e621.net/: Furry website supporting comics, including MLP + +We also support some other (file) types: +- epub: EPUB files created by this program (we do not support "all" EPUB files) +- text: Support class for local stories encoded in textual format, with a few rules : + - the title must be on the first line, + - the author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), + - chapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number, + - a description of the story must be given as chapter number 0, + - a cover image may be present with the same filename but a PNG, JPEG or JPG extension. +- info_text: Contains the same information as the TEXT format, but with a companion ".info" file to store some metadata +- cbz: CBZ (collection of images) files created with this program + +## TODO + +- A nice README file +- A binary JAR release (and thus, versions) +- Improve the CLI reader +- Offer some other readers (TUI, GUI) +- Check if it can work on Android + diff --git a/configure.sh b/configure.sh new file mode 100755 index 00000000..2eab91b5 --- /dev/null +++ b/configure.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# default: +PREFIX=/usr/local +PROGS="java javac jar make sed" + +valid=true +while [ "$*" != "" ]; do + key=`echo "$1" | cut -c1-9` + val=`echo "$1" | cut -c10-` + case "$key" in + --prefix=) + PREFIX="$val" + ;; + *) + echo "Unsupported parameter: '$1'" >&2 + valid=false + ;; + esac + shift +done + +[ $valid = false ] && exit 1 + +MESS="A required program cannot be found:" +for prog in $PROGS; do + out="`whereis -b "$prog" 2>/dev/null`" + if [ "$out" = "$prog:" ]; then + echo "$MESS $prog" >&2 + valid=false + fi +done + +[ $valid = false ] && exit 2 + +echo "MAIN = be/nikiroo/fanfix/Main" > Makefile +echo "NAME = fanfix" >> Makefile +echo "PREFIX = $PREFIX" >> Makefile +echo "JAR_FLAGS += -C bin/ org -C bin/ be" >> Makefile + +cat Makefile.base >> Makefile + diff --git a/libs/nikiroo-utils-0.9.1-sources.jar b/libs/nikiroo-utils-0.9.1-sources.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a9468a61ec46170cedbd8385b7395bbbda05cce GIT binary patch literal 16886 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%0DtQhq+ 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ⅆ0<5tmW-{s7F?(_$ni>R4HmL^)5|odC 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

$s|(-npB@E+iQv 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%0AjnmiAXjleQi9<4zbbj`tltgt?^QAXYJ9JM z)?(pcWBoTmx>Qh5e*&?kC>Q{s`b!%0$?DetQXrIn>3UmUC`SPVsN0}S^cXWG`jYwpktt|f*jDdRmrvji33Z4UgN`k(>lvlU1{D;BXq?ET>1PX0M z=FdqGY2a7Z{E7D4fzx|_e98^t^fHKQkB27Q=ieZ*u>4`6vYVBmjirO6HP8`gUX4QkG+XT>{A_a$Xb$>5Y5g^geD~m=Hvg75(#7cF0ia2k7zzLYP0>Fz zoW0*5ir5&N*xCTC9YNz9Xl&qU^J`n_d|^UGK{J03$Iq0!I`|FpBVYW|0T|nPw7Q@Q zd)^>gQvP5p01$QikD$M2dGRou)E%hz(I84bj+zi8*sp!c!u-h6Uq$VAynmd9GU0#L zVD9(};wQcgQ)&bCdI|eyS5EvIKmvsFhl#)MHymA^J~n8&Ur+`BXdk(91MQbq{RR4e zZ2e_~3PbsNgFzIv1G)2Yc2lDN7uesDbW{1z^3xLl;IsJ8%K|3m-(s@=Ym9yzS)wy> zd%B>Ni$?p;W?S<8R>dD0ewz5mW@#p+Wr1meEzjQ5knDz_>Xhv3l0{+wL z=-%?TcpU%ghTl;Y6u=)^K<>{fj2!;HfOCo_*v13RLx^|)0MifB0KofS%8NKSNE(}1 zI|5CCCiefvSS|ReI4Tgec0t(>v>g1QF_itvI1sq9qdm~t><{4I(hF?Q`Jx6iFs)2~ zUJ@2p{Ra1EqkqeRc}S$G5s)+HUi`dpZ|wOE_8->%k^yj_!YtaLoZ74SGfSseeuMU} z%RP}&deiryW1pxd{euMd=SAU;i?ptu}=s@AR1BL7HO8edQZ-9Sx>u*CA zpV@A93@U{Fp!72paPEGC{iD&3!u31;KiR@CKv~~R3O65{Ev!4az zPpYw>yB_~fFy9NVEB^=aA24-H&&+KgOS6{U7lEs~A4cVQg|g`G# U+10FFFF (was throwing IllegalArgumentException). +- Fixed HTML unescape for codepoints > Integer.MAX_VALUE (was throwing ArrayIndexOutOfBounds). +- Simplified and improved performance of codepoint-computing code by using Character.codePointAt(...) instead + of a complex conditional structure based on Character.isHighSurrogate(...) and Character.isLowSurrogate(...). +- [doc] Fixed description of MSExcel-compatible CSV files. + + +1.1.0.RELEASE +============= +- Added URI/URL escape and unescape operations. + + +1.0 +=== +- First release of unbescape. diff --git a/libs/unbescape-1.1.4_LICENSE.txt b/libs/unbescape-1.1.4_LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/libs/unbescape-1.1.4_LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/.gitattributes b/src/.gitattributes new file mode 100644 index 00000000..409851ff --- /dev/null +++ b/src/.gitattributes @@ -0,0 +1,49 @@ +# Auto detect text files and perform LF normalization +* text=auto + +*.java text diff=java +*.properties text +*.js text +*.css text +*.less text +*.html text diff=html +*.jsp text diff=html +*.jspx text diff=html +*.tag text diff=html +*.tagx text diff=html +*.tld text +*.xml text +*.gradle text + +*.sql text + +*.xsd text +*.dtd text +*.mod text +*.ent text + +*.txt text +*.md text +*.markdown text + +*.thtest text +*.thindex text +*.common text + +*.odt binary +*.pdf binary + +*.sh text eol=lf +*.bat text eol=crlf + +*.ico binary +*.png binary +*.svg binary +*.woff binary + +*.rar binary +*.zargo binary +*.zip binary + +CNAME text +*.MF text diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 00000000..5c798347 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,8 @@ +.classpath +.project +target/ +bin/ +.settings/ +.idea/ +*.iml + diff --git a/src/be/nikiroo/fanfix/Cache.java b/src/be/nikiroo/fanfix/Cache.java new file mode 100644 index 00000000..75a0f5d5 --- /dev/null +++ b/src/be/nikiroo/fanfix/Cache.java @@ -0,0 +1,427 @@ +package be.nikiroo.fanfix; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.FileAlreadyExistsException; +import java.util.Date; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import javax.imageio.ImageIO; + +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.supported.BasicSupport; +import be.nikiroo.utils.IOUtils; +import be.nikiroo.utils.MarkableFileInputStream; +import be.nikiroo.utils.StringUtils; + +/** + * This cache will manage Internet (and local) downloads, as well as put the + * downloaded files into a cache. + *

+ * As long the cached resource is not too old, it will use it instead of + * retrieving the file again. + * + * @author niki + */ +public class Cache { + private File dir; + private String UA; + private long tooOldChanging; + private long tooOldStable; + private CookieManager cookies; + + /** + * Create a new {@link Cache} object. + * + * @param dir + * the directory to use as cache + * @param UA + * the User-Agent to use to download the resources + * @param hoursChanging + * the number of hours after which a cached file that is thought + * to change ~often is considered too old (or -1 for + * "never too old") + * @param hoursStable + * the number of hours after which a LARGE cached file that is + * thought to change rarely is considered too old (or -1 for + * "never too old") + * + * @throws IOException + * in case of I/O error + */ + public Cache(File dir, String UA, int hoursChanging, int hoursStable) + throws IOException { + this.dir = dir; + this.UA = UA; + this.tooOldChanging = 1000 * 60 * 60 * hoursChanging; + this.tooOldStable = 1000 * 60 * 60 * hoursStable; + + if (dir != null) { + if (!dir.exists()) { + dir.mkdirs(); + } + } + + if (dir == null || !dir.exists()) { + throw new IOException("Cannot create the cache directory: " + + (dir == null ? "null" : dir.getAbsolutePath())); + } + + cookies = new CookieManager(); + cookies.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + CookieHandler.setDefault(cookies); + } + + /** + * Open a resource (will load it from the cache if possible, or save it into + * the cache after downloading if not). + * + * @param url + * the resource to open + * @param support + * the support to use to download the resource + * @param stable + * TRUE for more stable resources, FALSE when they often change + * + * @return the opened resource + * + * @throws IOException + * in case of I/O error + */ + public InputStream open(URL url, BasicSupport support, boolean stable) + throws IOException { + return open(url, support, stable, url); + } + + /** + * Open a resource (will load it from the cache if possible, or save it into + * the cache after downloading if not). + *

+ * The cached resource will be assimilated to the given original {@link URL} + * + * @param url + * the resource to open + * @param support + * the support to use to download the resource + * @param stable + * TRUE for more stable resources, FALSE when they often change + * @param originalUrl + * the original {@link URL} used to locate the cached resource + * + * @return the opened resource + * + * @throws IOException + * in case of I/O error + */ + public InputStream open(URL url, BasicSupport support, boolean stable, + URL originalUrl) throws IOException { + try { + InputStream in = load(originalUrl, false, stable); + if (in == null) { + try { + save(url, support, originalUrl); + } catch (IOException e) { + throw new IOException("Cannot save the url: " + + (url == null ? "null" : url.toString()), e); + } + + in = load(originalUrl, true, stable); + } + + return in; + } catch (IOException e) { + throw new IOException("Cannot open the url: " + + (url == null ? "null" : url.toString()), e); + } + } + + /** + * Refresh the resource into cache if needed. + * + * @param url + * the resource to open + * @param support + * the support to use to download the resource + * @param stable + * TRUE for more stable resources, FALSE when they often change + * + * @return TRUE if it was pre-downloaded + * + * @throws IOException + * in case of I/O error + */ + public void refresh(URL url, BasicSupport support, boolean stable) + throws IOException { + File cached = getCached(url); + if (cached.exists() && !isOld(cached, stable)) { + return; + } + + open(url, support, stable).close(); + } + + /** + * Check the resource to see if it is in the cache. + * + * @param url + * the resource to check + * + * @return TRUE if it is + * + */ + public boolean check(URL url) { + return getCached(url).exists(); + } + + /** + * Open a resource (will load it from the cache if possible, or save it into + * the cache after downloading if not) as an Image, then save it where + * requested. + *

+ * This version will not always work properly if the original file was not + * downloaded before. + * + * @param url + * the resource to open + * + * @return the opened resource image + * + * @throws IOException + * in case of I/O error + */ + public void saveAsImage(URL url, File target) throws IOException { + URL cachedUrl = new URL(url.toString() + + "." + + Instance.getConfig().getString(Config.IMAGE_FORMAT_CONTENT) + .toLowerCase()); + File cached = getCached(cachedUrl); + + if (!cached.exists() || isOld(cached, true)) { + InputStream imageIn = Instance.getCache().open(url, null, true); + ImageIO.write(StringUtils.toImage(imageIn), Instance.getConfig() + .getString(Config.IMAGE_FORMAT_CONTENT).toLowerCase(), + cached); + } + + IOUtils.write(new FileInputStream(cached), target); + } + + /** + * Manually add this item to the cache. + * + * @param in + * the input data + * @param uniqueID + * a unique ID for this resource + * + * @return the resulting {@link FileAlreadyExistsException} + * + * @throws IOException + * in case of I/O error + */ + public File addToCache(InputStream in, String uniqueID) throws IOException { + File file = getCached(new File(uniqueID).toURI().toURL()); + IOUtils.write(in, file); + return file; + } + + /** + * Clean the cache (delete the cached items). + * + * @param onlyOld + * only clean the files that are considered too old + * + * @return the number of cleaned items + */ + public int cleanCache(boolean onlyOld) { + int num = 0; + for (File file : dir.listFiles()) { + if (!onlyOld || isOld(file, true)) { + if (file.delete()) { + num++; + } else { + System.err.println("Cannot delete temporary file: " + + file.getAbsolutePath()); + } + } + } + return num; + } + + /** + * Open a resource from the cache if it exists. + * + * @param url + * the resource to open + * @return the opened resource + * @throws IOException + * in case of I/O error + */ + private InputStream load(URL url, boolean allowOld, boolean stable) + throws IOException { + File cached = getCached(url); + if (cached.exists() && !isOld(cached, stable)) { + return new MarkableFileInputStream(new FileInputStream(cached)); + } + + return null; + } + + /** + * Save the given resource to the cache. + * + * @param url + * the resource + * @param support + * the {@link BasicSupport} used to download it + * @param originalUrl + * the original {@link URL} used to locate the cached resource + * + * @throws IOException + * in case of I/O error + * @throws URISyntaxException + */ + private void save(URL url, BasicSupport support, URL originalUrl) + throws IOException { + URLConnection conn = url.openConnection(); + + conn.setRequestProperty("User-Agent", UA); + conn.setRequestProperty("Cookie", generateCookies(support)); + conn.setRequestProperty("Accept-Encoding", "gzip"); + if (support != null) { + conn.setRequestProperty("Referer", support.getCurrentReferer() + .toString()); + conn.setRequestProperty("Host", support.getCurrentReferer() + .getHost()); + } + + conn.connect(); + + // Check if redirect + if (conn instanceof HttpURLConnection + && ((HttpURLConnection) conn).getResponseCode() / 100 == 3) { + String newUrl = conn.getHeaderField("Location"); + save(new URL(newUrl), support, originalUrl); + return; + } + + InputStream in = conn.getInputStream(); + if ("gzip".equals(conn.getContentEncoding())) { + in = new GZIPInputStream(in); + } + + try { + File cached = getCached(originalUrl); + BufferedOutputStream out = new BufferedOutputStream( + new FileOutputStream(cached)); + try { + byte[] buf = new byte[4096]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } finally { + out.close(); + } + } finally { + in.close(); + } + } + + /** + * Check if the {@link File} is too old according to + * {@link Cache#tooOldChanging}. + * + * @param file + * the file to check + * @param stable + * TRUE to denote files that are not supposed to change too often + * + * @return TRUE if it is + */ + private boolean isOld(File file, boolean stable) { + long max = tooOldChanging; + if (stable) { + max = tooOldStable; + } + + if (max < 0) { + return false; + } + + long time = new Date().getTime() - file.lastModified(); + if (time < 0) { + System.err.println("Timestamp in the future for file: " + + file.getAbsolutePath()); + } + + return time < 0 || time > max; + } + + /** + * Get the cache resource from the cache if it is present for this + * {@link URL}. + * + * @param url + * the url + * @return the cached version if present, NULL if not + */ + private File getCached(URL url) { + String name = url.getHost(); + if (name == null || name.length() == 0) { + name = url.getFile(); + } else { + name = url.toString(); + } + + name = name.replace('/', '_').replace(':', '_'); + + return new File(dir, name); + } + + /** + * Generate the cookie {@link String} from the local {@link CookieStore} so + * it is ready to be passed. + * + * @return the cookie + */ + private String generateCookies(BasicSupport support) { + StringBuilder builder = new StringBuilder(); + for (HttpCookie cookie : cookies.getCookieStore().getCookies()) { + if (builder.length() > 0) { + builder.append(';'); + } + + // TODO: check if format is ok + builder.append(cookie.toString()); + } + + if (support != null) { + for (Map.Entry set : support.getCookies() + .entrySet()) { + if (builder.length() > 0) { + builder.append(';'); + } + builder.append(set.getKey()); + builder.append('='); + builder.append(set.getValue()); + } + } + + return builder.toString(); + } +} diff --git a/src/be/nikiroo/fanfix/Instance.java b/src/be/nikiroo/fanfix/Instance.java new file mode 100644 index 00000000..5c198ee7 --- /dev/null +++ b/src/be/nikiroo/fanfix/Instance.java @@ -0,0 +1,195 @@ +package be.nikiroo.fanfix; + +import java.io.File; +import java.io.IOException; + +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.ConfigBundle; +import be.nikiroo.fanfix.bundles.StringIdBundle; +import be.nikiroo.utils.resources.Bundles; + +/** + * Global state for the program (services and singletons). + * + * @author niki + */ +public class Instance { + private static ConfigBundle config; + private static StringIdBundle trans; + private static Cache cache; + private static Library lib; + private static boolean debug; + private static File coverDir; + + static { + config = new ConfigBundle(); + + // config dependent: + trans = new StringIdBundle(getLang()); + lib = new Library(getFile(Config.LIBRARY_DIR)); + debug = Instance.getConfig().getBoolean(Config.DEBUG_ERR, false); + coverDir = getFile(Config.DEFAULT_COVERS_DIR); + + if (coverDir != null && !coverDir.exists()) { + syserr(new IOException( + "The 'default covers' directory does not exists: " + + coverDir)); + coverDir = null; + } + // + + String noutf = System.getenv("NOUTF"); + if (noutf != null) { + noutf = noutf.trim().toLowerCase(); + if ("yes".equals(noutf) || "true".equals(noutf) + || "on".equals(noutf) || "1".equals(noutf) + || "y".equals(noutf)) { + trans.setUnicode(false); + } + } + + String configDir = System.getenv("CONFIG_DIR"); + if (configDir != null) { + if (new File(configDir).isDirectory()) { + Bundles.setDirectory(configDir); + try { + config = new ConfigBundle(); + config.updateFile(configDir); + } catch (IOException e) { + syserr(e); + } + try { + trans = new StringIdBundle(getLang()); + trans.updateFile(configDir); + } catch (IOException e) { + syserr(e); + } + } else { + syserr(new IOException("Configuration directory not found: " + + configDir)); + } + } + + try { + File tmp = getFile(Config.CACHE_DIR); + String ua = config.getString(Config.USER_AGENT); + int hours = config.getInteger(Config.CACHE_MAX_TIME_CHANGING, -1); + int hoursLarge = config + .getInteger(Config.CACHE_MAX_TIME_STABLE, -1); + + if (tmp == null) { + String tmpDir = System.getProperty("java.io.tmpdir"); + if (tmpDir != null) { + tmp = new File(tmpDir, "fanfic-tmp"); + } else { + syserr(new IOException( + "The system does not have a default temporary directory")); + } + } + + cache = new Cache(tmp, ua, hours, hoursLarge); + } catch (IOException e) { + syserr(new IOException( + "Cannot create cache (will continue without cache)", e)); + } + } + + /** + * Get the (unique) configuration service for the program. + * + * @return the configuration service + */ + public static ConfigBundle getConfig() { + return config; + } + + /** + * Get the (unique) {@link Cache} for the program. + * + * @return the {@link Cache} + */ + public static Cache getCache() { + return cache; + } + + /** + * Get the (unique) {link StringIdBundle} for the program. + * + * @return the {link StringIdBundle} + */ + public static StringIdBundle getTrans() { + return trans; + } + + /** + * Get the (unique) {@link Library} for the program. + * + * @return the {@link Library} + */ + public static Library getLibrary() { + return lib; + } + + /** + * Return the directory where to look for default cover pages. + * + * @return the default covers directory + */ + public static File getCoverDir() { + return coverDir; + } + + /** + * Report an error to the user + * + * @param e + * the {@link Exception} to report + */ + public static void syserr(Exception e) { + if (debug) { + e.printStackTrace(); + } else { + System.err.println(e.getMessage()); + } + } + + /** + * Return a path, but support the special $HOME variable. + * + * @return the path + */ + private static File getFile(Config id) { + File file = null; + String path = config.getString(id); + if (path != null && !path.isEmpty()) { + path = path.replace('/', File.separatorChar); + if (path.contains("$HOME")) { + path = path.replace("$HOME", + "" + System.getProperty("user.home")); + } + + file = new File(path); + } + + return file; + } + + /** + * The language to use for the application (NULL = default system language). + * + * @return the language + */ + private static String getLang() { + String lang = config.getString(Config.LANG); + + if (System.getenv("LANG") != null && !System.getenv("LANG").isEmpty()) { + lang = System.getenv("LANG"); + } + + if (lang != null && lang.isEmpty()) { + lang = null; + } + + return lang; + } +} diff --git a/src/be/nikiroo/fanfix/Library.java b/src/be/nikiroo/fanfix/Library.java new file mode 100644 index 00000000..9864ad77 --- /dev/null +++ b/src/be/nikiroo/fanfix/Library.java @@ -0,0 +1,261 @@ +package be.nikiroo.fanfix; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.output.BasicOutput; +import be.nikiroo.fanfix.output.BasicOutput.OutputType; +import be.nikiroo.fanfix.supported.BasicSupport; +import be.nikiroo.fanfix.supported.BasicSupport.SupportType; + +/** + * Manage a library of Stories: import, export, list. + *

+ * Each {@link Story} object will be associated with a (local to the library) + * unique ID, the LUID, which will be used to identify the {@link Story}. + * + * @author niki + */ +public class Library { + private File baseDir; + private Map stories; + private BasicSupport itSupport = BasicSupport + .getSupport(SupportType.INFO_TEXT); + private int lastId; + + /** + * Create a new {@link Library} with the given backend directory. + * + * @param dir + * the directoy where to find the {@link Story} objects + */ + public Library(File dir) { + this.baseDir = dir; + this.stories = new HashMap(); + this.lastId = 0; + + dir.mkdirs(); + } + + /** + * List all the stories of the given source type in the {@link Library}, or + * all the stories if NULL is passed as a type. + * + * @param type + * the type of story to retrieve, or NULL for all + * + * @return the stories + */ + public List getList(SupportType type) { + String typeString = type == null ? null : type.getSourceName(); + + List list = new ArrayList(); + for (Entry entry : getStories().entrySet()) { + String storyType = entry.getValue().getParentFile().getName(); + if (typeString == null || typeString.equalsIgnoreCase(storyType)) { + list.add(entry.getKey()); + } + } + + return list; + } + + /** + * Retrieve a specific {@link Story}. + * + * @param luid + * the Library UID of the story + * + * @return the corresponding {@link Story} + */ + public Story getStory(String luid) { + if (luid != null) { + for (Entry entry : getStories().entrySet()) { + if (luid.equals(entry.getKey().getLuid())) { + try { + return itSupport.process(entry.getValue().toURI() + .toURL()); + } catch (IOException e) { + // We should not have not-supported files in the + // library + Instance.syserr(new IOException( + "Cannot load file from library: " + + entry.getValue().getPath(), e)); + } + } + } + } + + return null; + } + + /** + * Import the {@link Story} at the given {@link URL} into the + * {@link Library}. + * + * @param url + * the {@link URL} to import + * + * @return the imported {@link Story} + * + * @throws IOException + * in case of I/O error + */ + public Story imprt(URL url) throws IOException { + BasicSupport support = BasicSupport.getSupport(url); + if (support == null) { + throw new IOException("URL not supported: " + url.toString()); + } + + getStories(); // refresh lastId + Story story = support.process(url); + story.getMeta().setLuid(String.format("%03d", (++lastId))); + save(story); + + return story; + } + + /** + * Export the {@link Story} to the given target in the given format. + * + * @param luid + * the {@link Story} ID + * @param type + * the {@link OutputType} to transform it to + * @param target + * the target to save to + * + * @return the saved resource (the main saved {@link File}) + * + * @throws IOException + * in case of I/O error + */ + public File export(String luid, OutputType type, String target) + throws IOException { + BasicOutput out = BasicOutput.getOutput(type, true); + if (out == null) { + throw new IOException("Output type not supported: " + type); + } + + return out.process(getStory(luid), target); + } + + /** + * Save a story as-is to the {@link Library} -- the LUID must be + * correct. + * + * @param story + * the {@link Story} to save + * + * @throws IOException + * in case of I/O error + */ + private void save(Story story) throws IOException { + MetaData key = story.getMeta(); + + getDir(key).mkdirs(); + if (!getDir(key).exists()) { + throw new IOException("Cannot create library dir"); + } + + OutputType out; + SupportType in; + if (key != null && key.isImageDocument()) { + in = SupportType.CBZ; + out = OutputType.CBZ; + } else { + in = SupportType.INFO_TEXT; + out = OutputType.INFO_TEXT; + } + BasicOutput it = BasicOutput.getOutput(out, true); + File file = it.process(story, getFile(key).getPath()); + getStories().put( + BasicSupport.getSupport(in).processMeta(file.toURI().toURL()) + .getMeta(), file); + } + + /** + * The directory (full path) where the {@link Story} related to this + * {@link MetaData} should be located on disk. + * + * @param key + * the {@link Story} {@link MetaData} + * + * @return the target directory + */ + private File getDir(MetaData key) { + String source = key.getSource().replaceAll("[^a-zA-Z0-9._+-]", "_"); + return new File(baseDir, source); + } + + /** + * The target (full path) where the {@link Story} related to this + * {@link MetaData} should be located on disk. + * + * @param key + * the {@link Story} {@link MetaData} + * + * @return the target + */ + private File getFile(MetaData key) { + String title = key.getTitle().replaceAll("[^a-zA-Z0-9._+-]", "_"); + return new File(getDir(key), key.getLuid() + "_" + title); + } + + /** + * Return all the known stories in this {@link Library} object. + * + * @return the stories + */ + private Map getStories() { + if (stories.isEmpty()) { + lastId = 0; + String format = Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER).toLowerCase(); + for (File dir : baseDir.listFiles()) { + if (dir.isDirectory()) { + for (File file : dir.listFiles()) { + try { + String path = file.getPath().toLowerCase(); + if (!path.endsWith(".info") + && !path.endsWith(format)) { + MetaData meta = itSupport.processMeta( + file.toURI().toURL()).getMeta(); + stories.put(meta, file); + + try { + int id = Integer.parseInt(meta.getLuid()); + if (id > lastId) { + lastId = id; + } + } catch (Exception e) { + // not normal!! + Instance.syserr(new IOException( + "Cannot read the LUID of: " + + file.getPath(), e)); + } + } + } catch (IOException e) { + // We should not have not-supported files in the + // library + Instance.syserr(new IOException( + "Cannot load file from library: " + + file.getPath(), e)); + } + } + } + } + } + + return stories; + } +} diff --git a/src/be/nikiroo/fanfix/Main.java b/src/be/nikiroo/fanfix/Main.java new file mode 100644 index 00000000..45b87c42 --- /dev/null +++ b/src/be/nikiroo/fanfix/Main.java @@ -0,0 +1,341 @@ +package be.nikiroo.fanfix; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.output.BasicOutput; +import be.nikiroo.fanfix.output.BasicOutput.OutputType; +import be.nikiroo.fanfix.reader.CliReader; +import be.nikiroo.fanfix.supported.BasicSupport; +import be.nikiroo.fanfix.supported.BasicSupport.SupportType; + +/** + * Main program entry point. + * + * @author niki + */ +public class Main { + /** + * Main program entry point. + *

+ * Known environment variables: + *

    + *
  • NOUTF: if set to 1, the program will prefer non-unicode + * {@link String}s when possible
  • + *
  • CONFIG_DIR: a path where to look for the .properties files + * before taking the included ones; they will also be saved/updated into + * this path when the program starts
  • + *
+ * + * @param args + *
    + *
  1. --import [URL]: import into library
  2. --export [id] + * [output_type] [target]: export story to target
  3. + * --convert [URL] [output_type] [target]: convert URL into + * target
  4. --read [id]: read the given story from the + * library
  5. --read-url [URL]: convert on the fly and read + * the story, without saving it
  6. --list: list the stories + * present in the library
  7. + *
+ */ + public static void main(String[] args) { + int exitCode = 255; + + if (args.length > 0) { + String action = args[0]; + if (action.equals("--import")) { + if (args.length > 1) { + exitCode = imprt(args[1]); + } + } else if (action.equals("--export")) { + if (args.length > 3) { + exitCode = export(args[1], args[2], args[3]); + } + } else if (action.equals("--convert")) { + if (args.length > 3) { + exitCode = convert( + args[1], + args[2], + args[3], + args.length > 4 ? args[4].toLowerCase().equals( + "+info") : false); + } + } else if (action.equals("--list")) { + exitCode = list(args.length > 1 ? args[1] : null); + } else if (action.equals("--read-url")) { + if (args.length > 1) { + exitCode = read(args[1], args.length > 2 ? args[2] : null, + false); + } + } else if (action.equals("--read")) { + if (args.length > 1) { + exitCode = read(args[1], args.length > 2 ? args[2] : null, + true); + } + } + } + + if (exitCode == 255) { + syntax(); + } + + if (exitCode != 0) { + System.exit(exitCode); + } + } + + /** + * Return an {@link URL} from this {@link String}, be it a file path or an + * actual {@link URL}. + * + * @param sourceString + * the source + * + * @return the corresponding {@link URL} + * + * @throws MalformedURLException + * if this is neither a file nor a conventional {@link URL} + */ + private static URL getUrl(String sourceString) throws MalformedURLException { + if (sourceString == null || sourceString.isEmpty()) { + throw new MalformedURLException("Empty url"); + } + + URL source = null; + try { + source = new URL(sourceString); + } catch (MalformedURLException e) { + File sourceFile = new File(sourceString); + source = sourceFile.toURI().toURL(); + } + + return source; + } + + /** + * Import the given resource into the {@link Library}. + * + * @param sourceString + * the resource to import + * + * @return the exit return code (0 = success) + */ + private static int imprt(String sourceString) { + try { + Story story = Instance.getLibrary().imprt(getUrl(sourceString)); + System.out.println(story.getMeta().getLuid() + ": \"" + + story.getMeta().getTitle() + "\" imported."); + } catch (IOException e) { + Instance.syserr(e); + return 1; + } + + return 0; + } + + /** + * Export the {@link Story} from the {@link Library} to the given target. + * + * @param sourceString + * the story LUID + * @param typeString + * the {@link OutputType} to use + * @param target + * the target + * + * @return the exit return code (0 = success) + */ + private static int export(String sourceString, String typeString, + String target) { + OutputType type = OutputType.valueOfNullOkUC(typeString); + if (type == null) { + Instance.syserr(new Exception(trans(StringId.OUTPUT_DESC, + typeString))); + return 1; + } + + try { + Story story = Instance.getLibrary().imprt(new URL(sourceString)); + Instance.getLibrary().export(story.getMeta().getLuid(), type, + target); + } catch (IOException e) { + Instance.syserr(e); + return 4; + } + + return 0; + } + + /** + * List the stories of the given type from the {@link Library} (unless NULL + * is passed, in which case all stories will be listed). + * + * @param typeString + * the {@link SupportType} to list the known stories of, or NULL + * to list all stories + * + * @return the exit return code (0 = success) + */ + private static int list(String typeString) { + SupportType type = null; + try { + type = SupportType.valueOfNullOkUC(typeString); + } catch (Exception e) { + Instance.syserr(new Exception( + trans(StringId.INPUT_DESC, typeString), e)); + return 1; + } + + CliReader.list(type); + + return 0; + } + + /** + * Start the CLI reader for this {@link Story}. + * + * @param story + * the LUID of the {@link Story} in the {@link Library} or + * the {@link Story} {@link URL} + * @param chap + * which {@link Chapter} to read (starting at 1), or NULL to get + * the {@link Story} description + * @param library + * TRUE if the source is the {@link Story} LUID, FALSE if it is a + * {@link URL} + * + * @return the exit return code (0 = success) + */ + private static int read(String story, String chap, boolean library) { + try { + CliReader reader; + if (library) { + reader = new CliReader(story); + } else { + reader = new CliReader(getUrl(story)); + } + + if (chap != null) { + reader.read(Integer.parseInt(chap)); + } else { + reader.read(); + } + } catch (IOException e) { + Instance.syserr(e); + return 1; + } + + return 0; + } + + /** + * Convert the {@link Story} into another format. + * + * @param sourceString + * the source {@link Story} to convert + * @param typeString + * the {@link OutputType} to convert to + * @param filename + * the target file + * @param infoCover + * TRUE to also export the cover and info file, even if the given + * {@link OutputType} does not usually save them + * + * @return the exit return code (0 = success) + */ + private static int convert(String sourceString, String typeString, + String filename, boolean infoCover) { + int exitCode = 0; + + String sourceName = sourceString; + try { + URL source = getUrl(sourceString); + sourceName = source.toString(); + if (source.toString().startsWith("file://")) { + sourceName = sourceName.substring("file://".length()); + } + + OutputType type = OutputType.valueOfAllOkUC(typeString); + if (type == null) { + Instance.syserr(new IOException(trans( + StringId.ERR_BAD_OUTPUT_TYPE, typeString))); + + exitCode = 2; + } else { + try { + BasicSupport support = BasicSupport.getSupport(source); + if (support != null) { + Story story = support.process(source); + + try { + filename = new File(filename).getAbsolutePath(); + BasicOutput.getOutput(type, infoCover).process( + story, filename); + } catch (IOException e) { + Instance.syserr(new IOException(trans( + StringId.ERR_SAVING, filename), e)); + exitCode = 5; + } + } else { + Instance.syserr(new IOException(trans( + StringId.ERR_NOT_SUPPORTED, source))); + + exitCode = 4; + } + } catch (IOException e) { + Instance.syserr(new IOException(trans(StringId.ERR_LOADING, + sourceName), e)); + exitCode = 3; + } + } + } catch (MalformedURLException e) { + Instance.syserr(new IOException(trans(StringId.ERR_BAD_URL, + sourceName), e)); + exitCode = 1; + } + + return exitCode; + } + + /** + * Simple shortcut method to call {link Instance#getTrans()#getString()}. + * + * @param id + * the ID to translate + * + * @return the translated result + */ + private static String trans(StringId id, Object... params) { + return Instance.getTrans().getString(id, params); + } + + /** + * Display the correct syntax of the program to the user. + */ + private static void syntax() { + StringBuilder builder = new StringBuilder(); + for (SupportType type : SupportType.values()) { + builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(), + type.getDesc())); + builder.append('\n'); + } + + String typesIn = builder.toString(); + builder.setLength(0); + + for (OutputType type : OutputType.values()) { + builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(), + type.getDesc())); + builder.append('\n'); + } + + String typesOut = builder.toString(); + + System.err.println(trans(StringId.ERR_SYNTAX, typesIn, typesOut)); + } +} diff --git a/src/be/nikiroo/fanfix/bundles/Config.java b/src/be/nikiroo/fanfix/bundles/Config.java new file mode 100644 index 00000000..969eb271 --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/Config.java @@ -0,0 +1,45 @@ +package be.nikiroo.fanfix.bundles; + +import be.nikiroo.utils.resources.Meta; + +/** + * The configuration options. + * + * @author niki + */ +public enum Config { + @Meta(what = "language", where = "", format = "language (example: en-GB) or nothing for default system language", info = "Force the language (can be overwritten again with the env variable $LANG)") + LANG, // + @Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to store temporary files, defaults to a directory 'fanfic-tmp' in the system default temporary directory") + CACHE_DIR, // + @Meta(what = "delay in hours", where = "", format = "integer | 0: no cache | -1: infinite time cache which is default", info = "The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh") + CACHE_MAX_TIME_CHANGING, // + @Meta(what = "delay in hours", where = "", format = "integer | 0: no cache | -1: infinite time cache which is default", info = "The delay after which a cached resource that is thought to change rarely is considered too old and triggers a refresh") + CACHE_MAX_TIME_STABLE, // + @Meta(what = "string", where = "", format = "", info = "The user-agent to use to download files") + USER_AGENT, // + @Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to get the default story covers") + DEFAULT_COVERS_DIR, // + @Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to store the library") + LIBRARY_DIR, // + @Meta(what = "boolean", where = "", format = "'true' or 'false'", info = "Show debug information on errors") + DEBUG_ERR, // + @Meta(what = "image format", where = "", format = "PNG, JPG, BMP...", info = "Image format to use for cover images") + IMAGE_FORMAT_COVER, // + @Meta(what = "image format", where = "", format = "PNG, JPG, BMP...", info = "Image format to use for content images") + IMAGE_FORMAT_CONTENT, // + @Meta(what = "", where = "", format = "not used", info = "This key is only present to allow access to suffixes") + LATEX_LANG, // + @Meta(what = "LaTeX output language", where = "LaTeX", format = "", info = "LaTeX full name for English") + LATEX_LANG_EN, // + @Meta(what = "LaTeX output language", where = "LaTeX", format = "", info = "LaTeX full name for French") + LATEX_LANG_FR, // + @Meta(what = "other 'by' prefixes before author name", where = "", format = "coma-separated list", info = "used to identify the author") + BYS, // + @Meta(what = "Chapter identification languages", where = "", format = "coma-separated list", info = "used to identify a starting chapter in text mode") + CHAPTER, // + @Meta(what = "Chapter identification string", where = "", format = "", info = "used to identify a starting chapter in text mode") + CHAPTER_EN, // + @Meta(what = "Chapter identification string", where = "", format = "", info = "used to identify a starting chapter in text mode") + CHAPTER_FR, // +} diff --git a/src/be/nikiroo/fanfix/bundles/ConfigBundle.java b/src/be/nikiroo/fanfix/bundles/ConfigBundle.java new file mode 100644 index 00000000..4f2303ec --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/ConfigBundle.java @@ -0,0 +1,38 @@ +package be.nikiroo.fanfix.bundles; + +import java.io.File; +import java.io.IOException; + +import be.nikiroo.utils.resources.bundles.Bundle; + +/** + * This class manages the configuration of the application. + * + * @author niki + */ +public class ConfigBundle extends Bundle { + public ConfigBundle() { + super(Config.class, Target.config); + } + + /** + * Update resource file. + * + * @param args + * not used + * + * @throws IOException + * in case of I/O error + */ + public static void main(String[] args) throws IOException { + String path = new File(".").getAbsolutePath() + + "/src/be/nikiroo/fanfix/bundles/"; + new ConfigBundle().updateFile(path); + System.out.println("Path updated: " + path); + } + + @Override + protected String getBundleDisplayName() { + return "Configuration options"; + } +} diff --git a/src/be/nikiroo/fanfix/bundles/StringId.java b/src/be/nikiroo/fanfix/bundles/StringId.java new file mode 100644 index 00000000..39cb0b23 --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/StringId.java @@ -0,0 +1,122 @@ +package be.nikiroo.fanfix.bundles; + +import java.io.IOException; +import java.io.Writer; + +import be.nikiroo.utils.resources.Meta; +import be.nikiroo.utils.resources.bundles.Bundle; + +/** + * The {@link Enum} representing textual information to be translated to the + * user as a key. + * + * Note that each key that should be translated must be annotated with a + * {@link Meta} annotation. + * + * @author niki + */ +public enum StringId { + /** + * A special key used for technical reasons only, without annotations so it + * is not visible in .properties files. + *

+ * Use it when you need NO translation. + */ + NULL, // + /** + * A special key used for technical reasons only, without annotations so it + * is not visible in .properties files. + *

+ * Use it when you need a real translation but still don't have a key. + */ + DUMMY, // + @Meta(what = "error message", where = "cli", format = "%s = supported input, %s = supported output", info = "syntax error message") + ERR_SYNTAX, // + @Meta(what = "error message", where = "cli", format = "%s = support name, %s = support desc", info = "an input or output support type description") + ERR_SYNTAX_TYPE, // + @Meta(what = "error message", where = "cli", format = "%s = input string", info = "Error when retrieving data") + ERR_LOADING, // + @Meta(what = "error message", where = "cli", format = "%s = save target", info = "Error when saving to given target") + ERR_SAVING, // + @Meta(what = "error message", where = "cli", format = "%s = bad output format", info = "Error when unknown output format") + ERR_BAD_OUTPUT_TYPE, // + @Meta(what = "error message", where = "cli", format = "%s = input string", info = "Error when converting input to URL/File") + ERR_BAD_URL, // + @Meta(what = "error message", where = "cli", format = "%s = input url", info = "URL/File not supported") + ERR_NOT_SUPPORTED, // + @Meta(what = "error message", where = "BasicSupport", format = "%s = cover URL", info = "Failed to download cover : %s") + ERR_BS_NO_COVER, // + @Meta(what = "char", where = "LaTeX/BasicSupport", format = "single char", info = "Canonical OPEN SINGLE QUOTE char (for instance: `)") + OPEN_SINGLE_QUOTE, // + @Meta(what = "char", where = "LaTeX/BasicSupport", format = "single char", info = "Canonical CLOSE SINGLE QUOTE char (for instance: ‘)") + CLOSE_SINGLE_QUOTE, // + @Meta(what = "char", where = "LaTeX/BasicSupport", format = "single char", info = "Canonical OPEN DOUBLE QUOTE char (for instance: “)") + OPEN_DOUBLE_QUOTE, // + @Meta(what = "char", where = "LaTeX/BasicSupport", format = "single char", info = "Canonical CLOSE DOUBLE QUOTE char (for instance: ”)") + CLOSE_DOUBLE_QUOTE, // + @Meta(what = "chapter name", where = "BasicSupport", format = "", info = "Name of the description fake chapter") + DESCRIPTION, // + @Meta(what = "chapter name", where = "", format = "%d = number, %s = name", info = "Name of a chapter with a name") + CHAPTER_NAMED, // + @Meta(what = "chapter name", where = "", format = "%d = number, %s = name", info = "Name of a chapter without name") + CHAPTER_UNNAMED, // + @Meta(what = "input format description", where = "SupportType", format = "%s = type", info = "Default description when the type is not known by i18n") + INPUT_DESC, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_EPUB, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_TEXT, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_INFO_TEXT, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_FANFICTION, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_FIMFICTION, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_MANGAFOX, // + @Meta(what = "input format description", where = "SupportType", format = "", info = "Description of this input type") + INPUT_DESC_E621, // + @Meta(what = "output format description", where = "OutputType", format = "%s = type", info = "Default description when the type is not known by i18n") + OUTPUT_DESC, // + @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_EPUB, // + @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_TEXT, // + @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_INFO_TEXT, // + @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_CBZ, // + @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_LATEX, // + @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SYSOUT, // + @Meta(what = "error message", where = "LaTeX", format = "%s = the unknown 2-code language", info = "Error message for unknown 2-letter LaTeX language code") + LATEX_LANG_UNKNOWN, // + @Meta(what = "'by' prefix before author name", where = "", format = "", info = "used to output the author, make sure it is covered by Config.BYS for input detection") + BY, // + + ; + + /** + * Write the header found in the configuration .properties file of + * this {@link Bundle}. + * + * @param writer + * the {@link Writer} to write the header in + * @param name + * the file name + * + * @throws IOException + * in case of IO error + */ + static public void writeHeader(Writer writer, String name) + throws IOException { + writer.write("# " + name + " translation file (UTF-8)\n"); + writer.write("# \n"); + writer.write("# Note that any key can be doubled with a _NOUTF suffix\n"); + writer.write("# to use when the NOUTF env variable is set to 1\n"); + writer.write("# \n"); + writer.write("# Also, the comments always refer to the key below them.\n"); + writer.write("# \n"); + } +}; diff --git a/src/be/nikiroo/fanfix/bundles/StringIdBundle.java b/src/be/nikiroo/fanfix/bundles/StringIdBundle.java new file mode 100644 index 00000000..3456b67e --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/StringIdBundle.java @@ -0,0 +1,40 @@ +package be.nikiroo.fanfix.bundles; + +import java.io.File; +import java.io.IOException; + +import be.nikiroo.utils.resources.bundles.TransBundle; + +/** + * This class manages the translation resources of the application. + * + * @author niki + */ +public class StringIdBundle extends TransBundle { + /** + * Create a translation service for the given language (will fall back to + * the default one i not found). + * + * @param lang + * the language to use + */ + public StringIdBundle(String lang) { + super(StringId.class, Target.resources, lang); + } + + /** + * Update resource file. + * + * @param args + * not used + * + * @throws IOException + * in case of I/O error + */ + public static void main(String[] args) throws IOException { + String path = new File(".").getAbsolutePath() + + "/src/be/nikiroo/fanfix/bundles/"; + new StringIdBundle(null).updateFile(path); + System.out.println("Path updated: " + path); + } +} diff --git a/src/be/nikiroo/fanfix/bundles/Target.java b/src/be/nikiroo/fanfix/bundles/Target.java new file mode 100644 index 00000000..212f8a70 --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/Target.java @@ -0,0 +1,19 @@ +package be.nikiroo.fanfix.bundles; + +import be.nikiroo.utils.resources.bundles.Bundle; + +/** + * The type of configuration information the associated {@link Bundle} will + * convey. + * + * @author niki + */ +public enum Target { + /** + * Configuration options that the user can change in the + * .properties file. + */ + config, + /** Translation resources. */ + resources, +} diff --git a/src/be/nikiroo/fanfix/bundles/config.properties b/src/be/nikiroo/fanfix/bundles/config.properties new file mode 100644 index 00000000..8a1d6c1e --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/config.properties @@ -0,0 +1,55 @@ +# Configuration options +# + + +# (WHAT: language, FORMAT: language (example: en-GB) or nothing for default system language) +# Force the language (can be overwritten again with the env variable $LANG) +LANG = +# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator) +# The directory where to store temporary files, defaults to a directory 'fanfic-tmp' in the system default temporary directory +CACHE_DIR = +# (WHAT: delay in hours, FORMAT: integer | 0: no cache | -1: infinite time cache which is default) +# The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh +CACHE_MAX_TIME_CHANGING = 24 +# (WHAT: delay in hours, FORMAT: integer | 0: no cache | -1: infinite time cache which is default) +# The delay after which a cached resource that is thought to change rarely is considered too old and triggers a refresh +CACHE_MAX_TIME_STABLE = +# (WHAT: string) +# The user-agent to use to download files +USER_AGENT = Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0 -- ELinks/0.9.3 (Linux 2.6.11 i686; 79x24) +# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator) +# The directory where to get the default story covers +DEFAULT_COVERS_DIR = $HOME/bin/epub/ +# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator) +# The directory where to store the library +LIBRARY_DIR = $HOME/Books +# (WHAT: boolean, FORMAT: 'true' or 'false') +# Show debug information on errors +DEBUG_ERR = true +# (WHAT: image format, FORMAT: PNG, JPG, BMP...) +# Image format to use for cover images +IMAGE_FORMAT_COVER = png +# (WHAT: image format, FORMAT: PNG, JPG, BMP...) +# Image format to use for content images +IMAGE_FORMAT_CONTENT = png +# (FORMAT: not used) +# This key is only present to allow access to suffixes +LATEX_LANG = +# (WHAT: LaTeX output language, WHERE: LaTeX) +# LaTeX full name for English +LATEX_LANG_EN = english +# (WHAT: LaTeX output language, WHERE: LaTeX) +# LaTeX full name for French +LATEX_LANG_FR = french +# (WHAT: other 'by' prefixes before author name, FORMAT: coma-separated list) +# used to identify the author +BYS = by,par,de,©,(c) +# (WHAT: Chapter identification languages, FORMAT: coma-separated list) +# used to identify a starting chapter in text mode +CHAPTER = EN,FR +# (WHAT: Chapter identification string) +# used to identify a starting chapter in text mode +CHAPTER_EN = Chapter +# (WHAT: Chapter identification string) +# used to identify a starting chapter in text mode +CHAPTER_FR = Chapitre diff --git a/src/be/nikiroo/fanfix/bundles/package-info.java b/src/be/nikiroo/fanfix/bundles/package-info.java new file mode 100644 index 00000000..50db0118 --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/package-info.java @@ -0,0 +1,8 @@ +/** + * This package encloses the different + * {@link be.nikiroo.utils.resources.bundles.Bundle} and their associated + * {@link java.lang.Enum}s used by the application. + * + * @author niki + */ +package be.nikiroo.fanfix.bundles; \ No newline at end of file diff --git a/src/be/nikiroo/fanfix/bundles/resources.properties b/src/be/nikiroo/fanfix/bundles/resources.properties new file mode 100644 index 00000000..13f6c2f9 --- /dev/null +++ b/src/be/nikiroo/fanfix/bundles/resources.properties @@ -0,0 +1,134 @@ +# United Kingdom (en_GB) resources translation file (UTF-8) +# +# Note that any key can be doubled with a _NOUTF suffix +# to use when the NOUTF env variable is set to 1 +# +# Also, the comments always refer to the key below them. +# + + +# (WHAT: error message, WHERE: cli, FORMAT: %s = supported input, %s = supported output) +# syntax error message +ERR_SYNTAX = Syntax error\n\ +\n\ +Valid options:\n\ +\t--import [URL]: import into library\n\ +\t--export [id] [output_type] [target]: export story to target\n\ +\t--convert [URL] [output_type] [target]: convert URL into target\n\ +\t--read [id]: read the given story from the library\n\ +\t--read-url [URL]: convert on the fly and read the story, without saving it\n\ +\t--list: list the stories present in the library\n\ +\n\ +Supported input types:\n\ +%s\n\ +\n\ +Supported output types:\n\ +%s +# (WHAT: error message, WHERE: cli, FORMAT: %s = support name, %s = support desc) +# an input or output support type description +ERR_SYNTAX_TYPE = %s: %s +# (WHAT: error message, WHERE: cli, FORMAT: %s = input string) +# Error when retrieving data +ERR_LOADING = Error when retrieving data from: %s +# (WHAT: error message, WHERE: cli, FORMAT: %s = save target) +# Error when saving to given target +ERR_SAVING = Error when saving to target: %s +# (WHAT: error message, WHERE: cli, FORMAT: %s = bad output format) +# Error when unknown output format +ERR_BAD_OUTPUT_TYPE = Unknown output type: %s +# (WHAT: error message, WHERE: cli, FORMAT: %s = input string) +# Error when converting input to URL/File +ERR_BAD_URL = Cannot understand file or protocol: %s +# (WHAT: error message, WHERE: cli, FORMAT: %s = input url) +# URL/File not supported +ERR_NOT_SUPPORTED = URL not supported: %s +# (WHAT: error message, WHERE: BasicSupport, FORMAT: %s = cover URL) +# Failed to download cover : %s +ERR_BS_NO_COVER = Failed to download cover: %s +# (WHAT: char, WHERE: LaTeX/BasicSupport, FORMAT: single char) +# Canonical OPEN SINGLE QUOTE char (for instance: `) +OPEN_SINGLE_QUOTE = ` +OPEN_SINGLE_QUOTE_NOUTF = ' +# (WHAT: char, WHERE: LaTeX/BasicSupport, FORMAT: single char) +# Canonical CLOSE SINGLE QUOTE char (for instance: ‘) +CLOSE_SINGLE_QUOTE = ‘ +CLOSE_SINGLE_QUOTE_NOUTF = ' +# (WHAT: char, WHERE: LaTeX/BasicSupport, FORMAT: single char) +# Canonical OPEN DOUBLE QUOTE char (for instance: “) +OPEN_DOUBLE_QUOTE = “ +OPEN_DOUBLE_QUOTE_NOUTF = " +# (WHAT: char, WHERE: LaTeX/BasicSupport, FORMAT: single char) +# Canonical CLOSE DOUBLE QUOTE char (for instance: ”) +CLOSE_DOUBLE_QUOTE = ” +CLOSE_DOUBLE_QUOTE_NOUTF = " +# (WHAT: chapter name, WHERE: BasicSupport) +# Name of the description fake chapter +DESCRIPTION = Description +# (WHAT: chapter name, FORMAT: %d = number, %s = name) +# Name of a chapter with a name +CHAPTER_NAMED = Chapter %d: %s +# (WHAT: chapter name, FORMAT: %d = number, %s = name) +# Name of a chapter without name +CHAPTER_UNNAMED = Chapter %d +# (WHAT: input format description, WHERE: SupportType, FORMAT: %s = type) +# Default description when the type is not known by i18n +INPUT_DESC = Unknown type: %s +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_EPUB = EPUB files created by this program (we do not support "all" EPUB files) +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_TEXT = Support class for local stories encoded in textual format, with a few rules :\n\ +\tthe title must be on the first line, \n\ +\tthe author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), \n\ +\tchapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number,\n\ +\ta description of the story must be given as chapter number 0,\n\ +\ta cover image may be present with the same filename but a PNG, JPEG or JPG extension. +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_INFO_TEXT = Contains the same information as the TEXT format, but with a companion ".info" file to store some metadata +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_FANFICTION = Fan fictions of many, many different universes, from TV shows to novels to games. +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_FIMFICTION = Fanfictions devoted to the My Little Pony show +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_MANGAFOX = A well filled repository of mangas, or, as their website states: Most popular manga scanlations read online for free at mangafox, as well as a close-knit community to chat and make friends. +# (WHAT: input format description, WHERE: SupportType) +# Description of this input type +INPUT_DESC_E621 = Furry website supporting comics, including MLP +# (WHAT: output format description, WHERE: OutputType, FORMAT: %s = type) +# Default description when the type is not known by i18n +OUTPUT_DESC = Unknown type: %s +# (WHAT: output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_EPUB = Standard EPUB file working on most e-book readers and viewers +# (WHAT: output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_TEXT = Local stories encoded in textual format, with a few rules :\n\ +\tthe title must be on the first line, \n\ +\tthe author (preceded by nothing, "by " or "©") must be on the second line, possibly with the publication date in parenthesis (i.e., "By Unknown (3rd October 1998)"), \n\ +\tchapters must be declared with "Chapter x" or "Chapter x: NAME OF THE CHAPTER", where "x" is the chapter number,\n\ +\ta description of the story must be given as chapter number 0,\n\ +\ta cover image may be present with the same filename but a PNG, JPEG or JPG extension. +# (WHAT: output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_INFO_TEXT = Contains the same information as the TEXT format, but with a companion ".info" file to store some metadata +# (WHAT: output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_CBZ = CBZ file (basically a ZIP file containing images -- we store the images in PNG format) +# (WHAT: output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_LATEX = A LaTeX file using the "book" template +# (WHAT: output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SYSOUT = A simple DEBUG console output +# (WHAT: error message, WHERE: LaTeX, FORMAT: %s = the unknown 2-code language) +# Error message for unknown 2-letter LaTeX language code +LATEX_LANG_UNKNOWN = Unknown language: %s +# (WHAT: 'by' prefix before author name) +# used to output the author, make sure it is covered by Config.BYS for input detection +BY = © +BY_NOUTF = (c) diff --git a/src/be/nikiroo/fanfix/data/Chapter.java b/src/be/nikiroo/fanfix/data/Chapter.java new file mode 100644 index 00000000..55160634 --- /dev/null +++ b/src/be/nikiroo/fanfix/data/Chapter.java @@ -0,0 +1,102 @@ +package be.nikiroo.fanfix.data; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A chapter in the story (or the resume/description). + * + * @author niki + */ +public class Chapter implements Iterable { + private String name; + private int number; + private List paragraphs = new ArrayList(); + private List empty = new ArrayList(); + + /** + * Create a new {@link Chapter} with the given information. + * + * @param number + * the chapter number, or 0 for the description/resume. + * @param name + * the chapter name + */ + public Chapter(int number, String name) { + this.number = number; + this.name = name; + } + + /** + * The chapter name. + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * The chapter name. + * + * @param name + * the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * The chapter number, or 0 for the description/resume. + * + * @return the number + */ + public int getNumber() { + return number; + } + + /** + * The chapter number, or 0 for the description/resume. + * + * @param number + * the number to set + */ + public void setNumber(int number) { + this.number = number; + } + + /** + * The included paragraphs. + * + * @return the paragraphs + */ + public List getParagraphs() { + return paragraphs; + } + + /** + * The included paragraphs. + * + * @param paragraphes + * the paragraphs to set + */ + public void setParagraphs(List paragraphs) { + this.paragraphs = paragraphs; + } + + /** + * Get an iterator on the {@link Paragraph}s. + */ + public Iterator iterator() { + return paragraphs == null ? empty.iterator() : paragraphs.iterator(); + } + + /** + * Display a DEBUG {@link String} representation of this object. + */ + @Override + public String toString() { + return "Chapter " + number + ": " + name; + } +} diff --git a/src/be/nikiroo/fanfix/data/MetaData.java b/src/be/nikiroo/fanfix/data/MetaData.java new file mode 100644 index 00000000..3980e96b --- /dev/null +++ b/src/be/nikiroo/fanfix/data/MetaData.java @@ -0,0 +1,276 @@ +package be.nikiroo.fanfix.data; + +import java.awt.image.BufferedImage; +import java.util.List; + +/** + * The meta data associated to a {@link Story} object. + * + * @author niki + */ +public class MetaData { + private String title; + private String author; + private String date; + private Chapter resume; + private List tags; + private BufferedImage cover; + private String subject; + private String source; + private String uuid; + private String luid; + private String lang; + private String publisher; + private boolean imageDocument; + + /** + * The title of the story. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * The title of the story. + * + * @param title + * the title to set + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * The author of the story. + * + * @return the author + */ + public String getAuthor() { + return author; + } + + /** + * The author of the story. + * + * @param author + * the author to set + */ + public void setAuthor(String author) { + this.author = author; + } + + /** + * The story publication date. + * + * @return the date + */ + public String getDate() { + return date; + } + + /** + * The story publication date. + * + * @param date + * the date to set + */ + public void setDate(String date) { + this.date = date; + } + + /** + * The tags associated with this story. + * + * @return the tags + */ + public List getTags() { + return tags; + } + + /** + * The tags associated with this story. + * + * @param tags + * the tags to set + */ + public void setTags(List tags) { + this.tags = tags; + } + + /** + * The story resume (a.k.a. description). + * + * @return the resume + */ + public Chapter getResume() { + return resume; + } + + /** + * The story resume (a.k.a. description). + * + * @param resume + * the resume to set + */ + public void setResume(Chapter resume) { + this.resume = resume; + } + + /** + * The cover image of the story if any (can be NULL). + * + * @return the cover + */ + public BufferedImage getCover() { + return cover; + } + + /** + * The cover image of the story if any (can be NULL). + * + * @param cover + * the cover to set + */ + public void setCover(BufferedImage cover) { + this.cover = cover; + } + + /** + * The subject of the story (or instance, if it is a fanfiction, what is the + * original work; if it is a technical text, what is the technical + * subject...). + * + * @return the subject + */ + public String getSubject() { + return subject; + } + + /** + * The subject of the story (for instance, if it is a fanfiction, what is + * the original work; if it is a technical text, what is the technical + * subject...). + * + * @param subject + * the subject to set + */ + public void setSubject(String subject) { + this.subject = subject; + } + + /** + * The source of this story (where it was downloaded from). + * + * @return the source + */ + public String getSource() { + return source; + } + + /** + * The source of this story (where it was downloaded from). + * + * @param source + * the source to set + */ + public void setSource(String source) { + this.source = source; + } + + /** + * A unique value representing the story (it is often an URL). + * + * @return the uuid + */ + public String getUuid() { + return uuid; + } + + /** + * A unique value representing the story (it is often an URL). + * + * @param uuid + * the uuid to set + */ + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * A unique value representing the story in the local library. + * + * @return the luid + */ + public String getLuid() { + return luid; + } + + /** + * A unique value representing the story in the local library. + * + * @param uuid + * the luid to set + */ + public void setLuid(String luid) { + this.luid = luid; + } + + /** + * The 2-letter code language of this story. + * + * @return the lang + */ + public String getLang() { + return lang; + } + + /** + * The 2-letter code language of this story. + * + * @param lang + * the lang to set + */ + public void setLang(String lang) { + this.lang = lang; + } + + /** + * The story publisher (other the same as the source). + * + * @return the publisher + */ + public String getPublisher() { + return publisher; + } + + /** + * The story publisher (other the same as the source). + * + * @param publisher + * the publisher to set + */ + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + /** + * Document catering mostly to image files. + * + * @return the imageDocument state + */ + public boolean isImageDocument() { + return imageDocument; + } + + /** + * Document catering mostly to image files. + * + * @param imageDocument + * the imageDocument state to set + */ + public void setImageDocument(boolean imageDocument) { + this.imageDocument = imageDocument; + } +} diff --git a/src/be/nikiroo/fanfix/data/Paragraph.java b/src/be/nikiroo/fanfix/data/Paragraph.java new file mode 100644 index 00000000..feb949ca --- /dev/null +++ b/src/be/nikiroo/fanfix/data/Paragraph.java @@ -0,0 +1,104 @@ +package be.nikiroo.fanfix.data; + +import java.net.URL; + +/** + * A paragraph in a chapter of the story. + * + * @author niki + */ +public class Paragraph { + /** + * A paragraph type, that will dictate how the paragraph will be handled. + * + * @author niki + */ + public enum ParagraphType { + /** Normal paragraph (text) */ + NORMAL, + /** Blank line */ + BLANK, + /** A Break paragraph, i.e.: HR (Horizontal Line) or '* * *' or whatever */ + BREAK, + /** Quotation (dialogue) */ + QUOTE, + /** An image (no text) */ + IMAGE, + } + + private ParagraphType type; + private String content; + + /** + * Create a new {@link Paragraph} with the given values. + * + * @param type + * the {@link ParagraphType} + * @param content + * the content of this paragraph + */ + public Paragraph(ParagraphType type, String content) { + this.type = type; + this.content = content; + } + + /** + * Create a new {@link Paragraph} with the given image. + * + * @param support + * the support that will be used to fetch the image via + * {@link Paragraph#getContentImage()}. + * @param content + * the content image of this paragraph + */ + public Paragraph(URL imageUrl) { + this.type = ParagraphType.IMAGE; + this.content = imageUrl.toString(); + } + + /** + * The {@link ParagraphType}. + * + * @return the type + */ + public ParagraphType getType() { + return type; + } + + /** + * The {@link ParagraphType}. + * + * @param type + * the type to set + */ + public void setType(ParagraphType type) { + this.type = type; + } + + /** + * The content of this {@link Paragraph}. + * + * @return the content + */ + public String getContent() { + return content; + } + + /** + * The content of this {@link Paragraph}. + * + * @param content + * the content to set + */ + public void setContent(String content) { + this.content = content; + } + + /** + * Display a DEBUG {@link String} representation of this object. + */ + @Override + public String toString() { + return String.format("%s: [%s]", "" + type, "" + content); + } +} diff --git a/src/be/nikiroo/fanfix/data/Story.java b/src/be/nikiroo/fanfix/data/Story.java new file mode 100644 index 00000000..cb651191 --- /dev/null +++ b/src/be/nikiroo/fanfix/data/Story.java @@ -0,0 +1,103 @@ +package be.nikiroo.fanfix.data; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * The main data class, where the whole story resides. + * + * @author niki + */ +public class Story implements Iterable { + private MetaData meta; + private List chapters = new ArrayList(); + private List empty = new ArrayList(); + + /** + * The metadata about this {@link Story}. + * + * @return the meta + */ + public MetaData getMeta() { + return meta; + } + + /** + * The metadata about this {@link Story}. + * + * @param meta + * the meta to set + */ + public void setMeta(MetaData meta) { + this.meta = meta; + } + + /** + * The chapters of the story. + * + * @return the chapters + */ + public List getChapters() { + return chapters; + } + + /** + * The chapters of the story. + * + * @param chapters + * the chapters to set + */ + public void setChapters(List chapters) { + this.chapters = chapters; + } + + /** + * Get an iterator on the {@link Chapter}s. + */ + public Iterator iterator() { + return chapters == null ? empty.iterator() : chapters.iterator(); + } + + /** + * Display a DEBUG {@link String} representation of this object. + *

+ * This is not efficient, nor intended to be. + */ + @Override + public String toString() { + String title = ""; + if (meta != null && meta.getTitle() != null) { + title = meta.getTitle(); + } + + String tags = ""; + if (meta != null && meta.getTags() != null) { + for (String tag : meta.getTags()) { + if (!tags.isEmpty()) { + tags += ", "; + } + tags += tag; + } + } + + String resume = ""; + if (meta != null && meta.getResume() != null) { + for (Paragraph para : meta.getResume()) { + resume += "\n\t"; + resume += para.toString().substring(0, + Math.min(para.toString().length(), 120)); + } + resume += "\n"; + } + + String cover = (meta == null || meta.getCover() == null) ? "none" + : meta.getCover().getWidth() + "x" + + meta.getCover().getHeight(); + return String.format( + "Title: [%s]\nAuthor: [%s]\nDate: [%s]\nTags: [%s]\n" + + "Resume: [%s]\nCover: [%s]", title, meta == null ? "" + : meta.getAuthor(), meta == null ? "" : meta.getDate(), + tags, resume, cover); + } +} diff --git a/src/be/nikiroo/fanfix/data/package-info.java b/src/be/nikiroo/fanfix/data/package-info.java new file mode 100644 index 00000000..aaa02c3d --- /dev/null +++ b/src/be/nikiroo/fanfix/data/package-info.java @@ -0,0 +1,7 @@ +/** + * This package contains the data structure used by the program, without the + * logic behind them. + * + * @author niki + */ +package be.nikiroo.fanfix.data; \ No newline at end of file diff --git a/src/be/nikiroo/fanfix/output/BasicOutput.java b/src/be/nikiroo/fanfix/output/BasicOutput.java new file mode 100644 index 00000000..2e77eaeb --- /dev/null +++ b/src/be/nikiroo/fanfix/output/BasicOutput.java @@ -0,0 +1,427 @@ +package be.nikiroo.fanfix.output; + +import java.io.File; +import java.io.IOException; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.data.Paragraph.ParagraphType; + +/** + * This class is the base class used by the other output classes. It can be used + * outside of this package, and have static method that you can use to get + * access to the correct support class. + * + * @author niki + */ +public abstract class BasicOutput { + /** + * The supported output types for which we can get a {@link BasicOutput} + * object. + * + * @author niki + */ + public enum OutputType { + /** EPUB files created with this program */ + EPUB, + /** Pure text file with some rules */ + TEXT, + /** TEXT but with associated .info file */ + INFO_TEXT, + /** DEBUG output to console */ + SYSOUT, + /** ZIP with (PNG) images */ + CBZ, + /** LaTeX file with "book" template */ + LATEX; + public String toString() { + return super.toString().toLowerCase(); + } + + /** + * A description of this output type. + * + * @return the description + */ + public String getDesc() { + String desc = Instance.getTrans().getStringX(StringId.OUTPUT_DESC, + this.name()); + + if (desc == null) { + desc = Instance.getTrans() + .getString(StringId.OUTPUT_DESC, this); + } + + return desc; + } + + /** + * Call {@link OutputType#valueOf(String.toUpperCase())}. + * + * @param typeName + * the possible type name + * + * @return NULL or the type + */ + public static OutputType valueOfUC(String typeName) { + return OutputType.valueOf(typeName == null ? null : typeName + .toUpperCase()); + } + + /** + * Call {@link OutputType#valueOf(String.toUpperCase())} but return NULL + * for NULL instead of raising exception. + * + * @param typeName + * the possible type name + * + * @return NULL or the type + */ + public static OutputType valueOfNullOkUC(String typeName) { + if (typeName == null) { + return null; + } + + return OutputType.valueOfUC(typeName); + } + + /** + * Call {@link OutputType#valueOf(String.toUpperCase())} but return NULL + * in case of error instead of raising an exception. + * + * @param typeName + * the possible type name + * + * @return NULL or the type + */ + public static OutputType valueOfAllOkUC(String typeName) { + try { + return OutputType.valueOfUC(typeName); + } catch (Exception e) { + return null; + } + } + } + + /** The creator name (this program, by me!) */ + static final String EPUB_CREATOR = "Fanfix (by Niki)"; + + /** The current best name for an image */ + private String imageName; + private File targetDir; + private String targetName; + private OutputType type; + private boolean writeCover; + private boolean writeInfo; + + /** + * Process the {@link Story} into the given target. + * + * @param story + * the {@link Story} to export + * @param target + * the target where to save to (will not necessary be taken as is + * by the processor, for instance an extension can be added) + * + * @return the actual main target saved, which can be slightly different + * that the input one + * + * @throws IOException + * in case of I/O error + */ + public File process(Story story, String target) throws IOException { + target = new File(target).getAbsolutePath(); + File targetDir = new File(target).getParentFile(); + String targetName = new File(target).getName(); + + String ext = getDefaultExtension(); + if (ext != null && !ext.isEmpty()) { + if (targetName.toLowerCase().endsWith(ext)) { + targetName = targetName.substring(0, + targetName.length() - ext.length()); + } + } + + return process(story, targetDir, targetName); + } + + /** + * Process the {@link Story} into the given target. + *

+ * This method is expected to be overridden in most cases. + * + * @param story + * the {@link Story} to export + * @param targetDir + * the target dir where to save to + * @param targetName + * the target filename (will not necessary be taken as is by the + * processor, for instance an extension can be added) + * + * @return the actual main target saved, which can be slightly different + * that the input one + * + * @throws IOException + * in case of I/O error + */ + protected File process(Story story, File targetDir, String targetName) + throws IOException { + this.targetDir = targetDir; + this.targetName = targetName; + + writeStory(story); + + return null; + } + + /** + * The output type. + * + * @return the type + */ + public OutputType getType() { + return type; + } + + /** + * The output type. + * + * @param type + * the new type + * @param infoCover + * TRUE to enable the creation of a .info file and a cover if + * possible + * + * @return this + */ + protected BasicOutput setType(OutputType type, boolean writeCover, + boolean writeInfo) { + this.type = type; + this.writeCover = writeCover; + this.writeInfo = writeInfo; + + return this; + } + + /** + * The default extension to add to the output files. + *

+ * Cannot be NULL! + * + * @return the extension + */ + protected String getDefaultExtension() { + return ""; + } + + protected void writeStoryHeader(Story story) throws IOException { + } + + protected void writeChapterHeader(Chapter chap) throws IOException { + } + + protected void writeParagraphHeader(Paragraph para) throws IOException { + } + + protected void writeStoryFooter(Story story) throws IOException { + } + + protected void writeChapterFooter(Chapter chap) throws IOException { + } + + protected void writeParagraphFooter(Paragraph para) throws IOException { + } + + protected void writeStory(Story story) throws IOException { + String chapterNameNum = String.format("%03d", 0); + String paragraphNumber = String.format("%04d", 0); + imageName = paragraphNumber + "_" + chapterNameNum + ".png"; + + if (writeCover) { + InfoCover.writeCover(targetDir, targetName, story.getMeta()); + } + if (writeInfo) { + InfoCover.writeInfo(targetDir, targetName, story.getMeta()); + } + + writeStoryHeader(story); + for (Chapter chap : story) { + writeChapter(chap); + } + writeStoryFooter(story); + } + + protected void writeChapter(Chapter chap) throws IOException { + String chapterNameNum; + if (chap.getName() == null || chap.getName().isEmpty()) { + chapterNameNum = String.format("%03d", chap.getNumber()); + } else { + chapterNameNum = String.format("%03d", chap.getNumber()) + "_" + + chap.getName().replace(" ", "_"); + } + + int num = 0; + String paragraphNumber = String.format("%04d", num++); + imageName = chapterNameNum + "_" + paragraphNumber + ".png"; + + writeChapterHeader(chap); + for (Paragraph para : chap) { + paragraphNumber = String.format("%04d", num++); + imageName = chapterNameNum + "_" + paragraphNumber + ".png"; + writeParagraph(para); + } + writeChapterFooter(chap); + } + + protected void writeParagraph(Paragraph para) throws IOException { + writeParagraphHeader(para); + writeTextLine(para.getType(), para.getContent()); + writeParagraphFooter(para); + } + + protected void writeTextLine(ParagraphType type, String line) + throws IOException { + } + + /** + * Return the current best guess for an image name, based upon the current + * {@link Chapter} and {@link Paragraph}. + * + * @param prefix + * add the original target name as a prefix + * + * @return the guessed name + */ + protected String getCurrentImageBestName(boolean prefix) { + if (prefix) { + return targetName + "_" + imageName; + } + + return imageName; + } + + /** + * Return the given word or sentence as bold. + * + * @param word + * the input + * + * @return the bold output + */ + protected String enbold(String word) { + return word; + } + + /** + * Return the given word or sentence as italic. + * + * @param word + * the input + * + * @return the italic output + */ + protected String italize(String word) { + return word; + } + + /** + * Decorate the given text with bold and italic words, + * according to {@link BasicOutput#enbold(String)} and + * {@link BasicOutput#italize(String)}. + * + * @param text + * the input + * + * @return the decorated output + */ + protected String decorateText(String text) { + StringBuilder builder = new StringBuilder(); + + int bold = -1; + int italic = -1; + char prev = '\0'; + for (char car : text.toCharArray()) { + switch (car) { + case '*': + if (bold >= 0 && prev != ' ') { + String data = builder.substring(bold); + builder.setLength(bold); + builder.append(enbold(data)); + bold = -1; + } else if (bold < 0 + && (prev == ' ' || prev == '\0' || prev == '\n')) { + bold = builder.length(); + } else { + builder.append(car); + } + + break; + case '_': + if (italic >= 0 && prev != ' ') { + String data = builder.substring(italic); + builder.setLength(italic); + builder.append(enbold(data)); + italic = -1; + } else if (italic < 0 + && (prev == ' ' || prev == '\0' || prev == '\n')) { + italic = builder.length(); + } else { + builder.append(car); + } + + break; + default: + builder.append(car); + break; + } + + prev = car; + } + + if (bold >= 0) { + builder.insert(bold, '*'); + } + + if (italic >= 0) { + builder.insert(italic, '_'); + } + + return builder.toString(); + } + + /** + * Return a {@link BasicOutput} object compatible with the given + * {@link OutputType}. + * + * @param type + * the type + * @param infoCover + * force the .info file and the cover to be saved next + * to the main target file + * + * @return the {@link BasicOutput} + */ + public static BasicOutput getOutput(OutputType type, boolean infoCover) { + if (type != null) { + switch (type) { + case EPUB: + return new Epub().setType(type, infoCover, infoCover); + case TEXT: + return new Text().setType(type, true, infoCover); + case INFO_TEXT: + return new InfoText().setType(type, true, true); + case SYSOUT: + return new Sysout().setType(type, false, false); + case CBZ: + return new Cbz().setType(type, infoCover, infoCover); + case LATEX: + return new LaTeX().setType(type, infoCover, infoCover); + } + } + + return null; + } +} diff --git a/src/be/nikiroo/fanfix/output/Cbz.java b/src/be/nikiroo/fanfix/output/Cbz.java new file mode 100644 index 00000000..51cf732a --- /dev/null +++ b/src/be/nikiroo/fanfix/output/Cbz.java @@ -0,0 +1,88 @@ +package be.nikiroo.fanfix.output; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.utils.IOUtils; + +class Cbz extends BasicOutput { + private File dir; + + @Override + public File process(Story story, File targetDir, String targetName) + throws IOException { + String targetNameOrig = targetName; + targetName += getDefaultExtension(); + + File target = new File(targetDir, targetName); + + dir = File.createTempFile("fanfic-reader-cbz-dir", ".wip"); + dir.delete(); + dir.mkdir(); + + // will also save the images! + new InfoText().process(story, dir, targetNameOrig); + IOUtils.writeSmallFile(dir, "version", "3.0"); + + try { + super.process(story, targetDir, targetNameOrig); + } finally { + } + + IOUtils.zip(dir, target, true); + IOUtils.deltree(dir); + + return target; + } + + @Override + protected String getDefaultExtension() { + return ".cbz"; + } + + @Override + protected void writeStoryHeader(Story story) throws IOException { + MetaData meta = story.getMeta(); + + StringBuilder builder = new StringBuilder(); + if (meta != null && meta.getResume() != null) { + for (Paragraph para : story.getMeta().getResume()) { + builder.append(para.getContent()); + builder.append("\n"); + } + } + + FileWriter writer = new FileWriter(new File(dir, "URL")); + try { + if (meta != null) { + writer.write(meta.getUuid()); + } + writer.write("\n\n"); + writer.write(builder.toString()); + } finally { + writer.close(); + } + + writer = new FileWriter(new File(dir, "SUMMARY")); + try { + String title = ""; + if (meta != null && meta.getTitle() != null) { + title = meta.getTitle(); + } + + writer.write(title); + if (meta != null && meta.getAuthor() != null) { + writer.write("\n©"); + writer.write(meta.getAuthor()); + } + writer.write("\n\n"); + writer.write(builder.toString()); + } finally { + writer.close(); + } + } +} diff --git a/src/be/nikiroo/fanfix/output/Epub.java b/src/be/nikiroo/fanfix/output/Epub.java new file mode 100644 index 00000000..1706d8b1 --- /dev/null +++ b/src/be/nikiroo/fanfix/output/Epub.java @@ -0,0 +1,471 @@ +package be.nikiroo.fanfix.output; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.imageio.ImageIO; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.data.Paragraph.ParagraphType; +import be.nikiroo.utils.IOUtils; +import be.nikiroo.utils.StringUtils; + +class Epub extends BasicOutput { + private File tmpDir; + private FileWriter writer; + private boolean inDialogue = false; + private boolean inNormal = false; + private File images; + + @Override + public File process(Story story, File targetDir, String targetName) + throws IOException { + String targetNameOrig = targetName; + targetName += getDefaultExtension(); + + tmpDir = File.createTempFile("fanfic-reader-epub_", ".wip"); + tmpDir.delete(); + + if (!tmpDir.mkdir()) { + throw new IOException( + "Cannot create a temporary directory: no space left on device?"); + } + + // "Originals" + File data = new File(tmpDir, "DATA"); + data.mkdir(); + new InfoText().process(story, data, targetNameOrig); + IOUtils.writeSmallFile(data, "version", "3.0"); + + super.process(story, targetDir, targetNameOrig); + + // zip/epub + File epub = new File(targetDir, targetName); + IOUtils.zip(tmpDir, epub, true); + IOUtils.deltree(tmpDir); + tmpDir = null; + + return epub; + } + + @Override + protected String getDefaultExtension() { + return ".epub"; + } + + @Override + protected void writeStoryHeader(Story story) throws IOException { + File ops = new File(tmpDir, "OPS"); + ops.mkdirs(); + File css = new File(ops, "css"); + css.mkdirs(); + images = new File(ops, "images"); + images.mkdirs(); + File metaInf = new File(tmpDir, "META-INF"); + metaInf.mkdirs(); + + // "root" + IOUtils.writeSmallFile(tmpDir, "mimetype", "application/epub+zip"); + + // META-INF + String containerContent = "\n" + + "\n" + + "\t\n" + + "\t\t\n" + + "\t\n" + "\n"; + + IOUtils.writeSmallFile(metaInf, "container.xml", containerContent); + + // OPS/css + InputStream inStyle = getClass().getResourceAsStream("epub.style.css"); + if (inStyle == null) { + throw new IOException("Cannot find style.css resource"); + } + try { + IOUtils.write(inStyle, new File(css, "style.css")); + } finally { + inStyle.close(); + } + + // OPS/images + if (story.getMeta() != null && story.getMeta().getCover() != null) { + String format = Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER).toLowerCase(); + File file = new File(images, "cover." + format); + ImageIO.write(story.getMeta().getCover(), format, file); + } + + // OPS/* except chapters + IOUtils.writeSmallFile(ops, "epb.ncx", generateNcx(story)); + IOUtils.writeSmallFile(ops, "epb.opf", generateOpf(story)); + IOUtils.writeSmallFile(ops, "title.xml", generateTitleXml(story)); + + // Resume + if (story.getMeta() != null && story.getMeta().getResume() != null) { + writeChapter(story.getMeta().getResume()); + } + } + + @Override + protected void writeChapterHeader(Chapter chap) throws IOException { + String filename = String.format("%s%03d%s", "chapter-", + chap.getNumber(), ".xml"); + writer = new FileWriter(new File(tmpDir + "/OPS", filename)); + inDialogue = false; + inNormal = false; + try { + String title = "Chapter " + chap.getNumber(); + String nameOrNum = Integer.toString(chap.getNumber()); + if (chap.getName() != null && !chap.getName().isEmpty()) { + title += ": " + chap.getName(); + nameOrNum = chap.getName(); + } + + writer.write(""); + writer.write("\n"); + writer.write("\n"); + writer.write("\n"); + writer.write("\n " + StringUtils.xmlEscape(title) + + ""); + writer.write("\n "); + writer.write("\n"); + writer.write("\n"); + writer.write("\n

"); + writer.write("\n Chapter " + + chap.getNumber() + ": "); + writer.write("\n " + + StringUtils.xmlEscape(nameOrNum) + ""); + writer.write("\n

"); + writer.write("\n "); + writer.write("\n
\n"); + } catch (Exception e) { + writer.close(); + throw new IOException(e); + } + } + + @Override + protected void writeChapterFooter(Chapter chap) throws IOException { + try { + if (inDialogue) { + writer.write("
\n"); + inDialogue = false; + } + if (inNormal) { + writer.write(" \n"); + inNormal = false; + } + writer.write(" \n\n\n"); + } finally { + writer.close(); + writer = null; + } + } + + @Override + protected void writeParagraphHeader(Paragraph para) throws IOException { + if (para.getType() == ParagraphType.QUOTE && !inDialogue) { + writer.write("
\n"); + inDialogue = true; + } else if (para.getType() != ParagraphType.QUOTE && inDialogue) { + writer.write("
\n"); + inDialogue = false; + } + + if (para.getType() == ParagraphType.NORMAL && !inNormal) { + writer.write("
\n"); + inNormal = true; + } else if (para.getType() != ParagraphType.NORMAL && inNormal) { + writer.write("
\n"); + inNormal = false; + } + + switch (para.getType()) { + case BLANK: + writer.write("
"); + break; + case BREAK: + writer.write("
"); + break; + case NORMAL: + writer.write(" "); + break; + case QUOTE: + writer.write("
— "); + break; + case IMAGE: + File file = new File(images, getCurrentImageBestName(false)); + Instance.getCache().saveAsImage(new URL(para.getContent()), file); + writer.write(" "); + break; + } + } + + @Override + protected void writeParagraphFooter(Paragraph para) throws IOException { + switch (para.getType()) { + case NORMAL: + writer.write("\n"); + break; + case QUOTE: + writer.write("
\n"); + break; + default: + writer.write("\n"); + break; + } + } + + @Override + protected void writeTextLine(ParagraphType type, String line) + throws IOException { + switch (type) { + case QUOTE: + case NORMAL: + writer.write(decorateText(StringUtils.xmlEscape(line))); + break; + default: + break; + } + } + + @Override + protected String enbold(String word) { + return "" + word + ""; + } + + @Override + protected String italize(String word) { + return "" + word + ""; + } + + private String generateNcx(Story story) { + StringBuilder builder = new StringBuilder(); + + String title = ""; + String uuid = ""; + String author = ""; + if (story.getMeta() != null) { + MetaData meta = story.getMeta(); + uuid = meta.getUuid(); + author = meta.getAuthor(); + title = meta.getTitle(); + } + + builder.append(""); + builder.append("\n"); + builder.append("\n"); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n " + StringUtils.xmlEscape(title) + ""); + builder.append("\n "); + builder.append("\n "); + + builder.append("\n " + StringUtils.xmlEscape(author) + ""); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n Title Page"); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + + int navPoint = 2; // 1 is above + + if (story.getMeta() != null & story.getMeta().getResume() != null) { + Chapter chap = story.getMeta().getResume(); + generateNcx(chap, builder, navPoint++); + } + + for (Chapter chap : story) { + generateNcx(chap, builder, navPoint++); + } + + builder.append("\n "); + builder.append("\n\n"); + + return builder.toString(); + } + + private void generateNcx(Chapter chap, StringBuilder builder, int navPoint) { + String name; + if (chap.getName() != null && !chap.getName().isEmpty()) { + name = Instance.getTrans().getString(StringId.CHAPTER_NAMED, + chap.getNumber(), chap.getName()); + } else { + name = Instance.getTrans().getString(StringId.CHAPTER_UNNAMED, + chap.getNumber()); + } + + String nnn = String.format("%03d", (navPoint - 2)); + + builder.append("\n "); + builder.append("\n "); + builder.append("\n " + name + ""); + builder.append("\n "); + builder.append("\n "); + builder.append("\n \n"); + } + + private String generateOpf(Story story) { + StringBuilder builder = new StringBuilder(); + + String title = ""; + String uuid = ""; + String author = ""; + String date = ""; + String publisher = ""; + String subject = ""; + String source = ""; + String lang = ""; + if (story.getMeta() != null) { + MetaData meta = story.getMeta(); + title = meta.getTitle(); + uuid = meta.getUuid(); + author = meta.getAuthor(); + date = meta.getDate(); + publisher = meta.getPublisher(); + subject = meta.getSubject(); + source = meta.getSource(); + lang = meta.getLang(); + } + + builder.append(""); + builder.append("\n"); + builder.append("\n "); + builder.append("\n " + StringUtils.xmlEscape(title) + + ""); + builder.append("\n " + + StringUtils.xmlEscape(author) + ""); + builder.append("\n " + + StringUtils.xmlEscape(date) + ""); + builder.append("\n " + + StringUtils.xmlEscape(publisher) + ""); + builder.append("\n "); + builder.append("\n " + StringUtils.xmlEscape(subject) + + ""); + builder.append("\n " + StringUtils.xmlEscape(source) + + ""); + builder.append("\n Not for commercial use."); + builder.append("\n " + + StringUtils.xmlEscape(uuid) + ""); + builder.append("\n " + StringUtils.xmlEscape(lang) + + ""); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + for (int i = 0; i <= story.getChapters().size(); i++) { + String name = String.format("%s%03d", "chapter-", i); + builder.append("\n "); + } + + builder.append("\n "); + builder.append("\n "); + + builder.append("\n "); + + if (story.getMeta() != null && story.getMeta().getCover() != null) { + String format = Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER).toLowerCase(); + builder.append("\n "); + } + + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + builder.append("\n "); + for (int i = 0; i <= story.getChapters().size(); i++) { + String name = String.format("%s%03d", "chapter-", i); + builder.append("\n "); + } + builder.append("\n "); + builder.append("\n\n"); + + return builder.toString(); + } + + private String generateTitleXml(Story story) { + StringBuilder builder = new StringBuilder(); + + String title = ""; + String tags = ""; + String author = ""; + if (story.getMeta() != null) { + MetaData meta = story.getMeta(); + title = meta.getTitle(); + if (meta.getTags() != null) { + for (String tag : meta.getTags()) { + if (!tags.isEmpty()) { + tags += ", "; + } + tags += tag; + } + + if (!tags.isEmpty()) { + tags = "(" + tags + ")"; + } + } + author = meta.getAuthor(); + } + + String format = Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER).toLowerCase(); + + builder.append(""); + builder.append("\n"); + builder.append("\n"); + builder.append("\n"); + builder.append("\n " + StringUtils.xmlEscape(title) + ""); + builder.append("\n "); + builder.append("\n"); + builder.append("\n"); + builder.append("\n
"); + builder.append("\n

" + StringUtils.xmlEscape(title) + "

"); + builder.append("\n
" + + StringUtils.xmlEscape(tags) + "
"); + builder.append("\n
"); + builder.append("\n "); + builder.append("\n
"); + builder.append("\n
" + + StringUtils.xmlEscape(author) + "
"); + builder.append("\n
"); + builder.append("\n"); + builder.append("\n\n"); + + return builder.toString(); + } +} diff --git a/src/be/nikiroo/fanfix/output/InfoCover.java b/src/be/nikiroo/fanfix/output/InfoCover.java new file mode 100644 index 00000000..2280e2dc --- /dev/null +++ b/src/be/nikiroo/fanfix/output/InfoCover.java @@ -0,0 +1,86 @@ +package be.nikiroo.fanfix.output; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.data.MetaData; + +class InfoCover { + public static void writeInfo(File targetDir, String targetName, + MetaData meta) throws IOException { + File info = new File(targetDir, targetName + ".info"); + FileWriter infoWriter = new FileWriter(info); + + if (meta != null) { + try { + String tags = ""; + if (meta.getTags() != null) { + for (String tag : meta.getTags()) { + if (!tags.isEmpty()) { + tags += ", "; + } + tags += tag; + } + } + + String lang = meta.getLang(); + if (lang != null) { + lang = lang.toLowerCase(); + } + + writeMeta(infoWriter, "TITLE", meta.getTitle()); + writeMeta(infoWriter, "AUTHOR", meta.getAuthor()); + writeMeta(infoWriter, "DATE", meta.getDate()); + writeMeta(infoWriter, "SUBJECT", meta.getSubject()); + writeMeta(infoWriter, "SOURCE", meta.getSource()); + writeMeta(infoWriter, "TAGS", tags); + writeMeta(infoWriter, "UUID", meta.getUuid()); + writeMeta(infoWriter, "LUID", meta.getLuid()); + writeMeta(infoWriter, "LANG", lang); + writeMeta(infoWriter, "IMAGES_DOCUMENT", + meta.isImageDocument() ? "true" : "false"); + if (meta.getCover() != null) { + String format = Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER).toLowerCase(); + writeMeta(infoWriter, "COVER", targetName + "." + format); + } else { + writeMeta(infoWriter, "COVER", ""); + } + writeMeta(infoWriter, "EPUBCREATOR", BasicOutput.EPUB_CREATOR); + writeMeta(infoWriter, "PUBLISHER", meta.getPublisher()); + } finally { + infoWriter.close(); + } + } + } + + public static void writeCover(File targetDir, String targetName, + MetaData meta) { + if (meta != null && meta.getCover() != null) { + try { + String format = Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER).toLowerCase(); + ImageIO.write(meta.getCover(), format, new File(targetDir, + targetName + "." + format)); + } catch (IOException e) { + // Allow to continue without cover + Instance.syserr(new IOException( + "Failed to save the cover image", e)); + } + } + } + + private static void writeMeta(FileWriter writer, String key, String value) + throws IOException { + if (value == null) { + value = ""; + } + + writer.write(String.format("%s=\"%s\"\n", key, value.replace("\"", "'"))); + } +} diff --git a/src/be/nikiroo/fanfix/output/InfoText.java b/src/be/nikiroo/fanfix/output/InfoText.java new file mode 100644 index 00000000..69376856 --- /dev/null +++ b/src/be/nikiroo/fanfix/output/InfoText.java @@ -0,0 +1,74 @@ +package be.nikiroo.fanfix.output; + +import java.io.IOException; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.Paragraph.ParagraphType; + +class InfoText extends Text { + // quote chars + private char openQuote = Instance.getTrans().getChar( + StringId.OPEN_SINGLE_QUOTE); + private char closeQuote = Instance.getTrans().getChar( + StringId.CLOSE_SINGLE_QUOTE); + private char openDoubleQuote = Instance.getTrans().getChar( + StringId.OPEN_DOUBLE_QUOTE); + private char closeDoubleQuote = Instance.getTrans().getChar( + StringId.CLOSE_DOUBLE_QUOTE); + + @Override + protected String getDefaultExtension() { + return ""; + } + + @Override + protected void writeChapterHeader(Chapter chap) throws IOException { + writer.write("\n"); + + if (chap.getName() != null && !chap.getName().isEmpty()) { + writer.write(Instance.getTrans().getString(StringId.CHAPTER_NAMED, + chap.getNumber(), chap.getName())); + } else { + writer.write(Instance.getTrans().getString( + StringId.CHAPTER_UNNAMED, chap.getNumber())); + } + + writer.write("\n\n"); + } + + @Override + protected void writeTextLine(ParagraphType type, String line) + throws IOException { + switch (type) { + case NORMAL: + case QUOTE: + StringBuilder builder = new StringBuilder(); + for (char car : line.toCharArray()) { + if (car == '—') { + builder.append("---"); + } else if (car == '–') { + builder.append("--"); + } else if (car == openDoubleQuote) { + builder.append("\""); + } else if (car == closeDoubleQuote) { + builder.append("\""); + } else if (car == openQuote) { + builder.append("'"); + } else if (car == closeQuote) { + builder.append("'"); + } else { + builder.append(car); + } + } + + line = builder.toString(); + break; + default: + break; + } + + super.writeTextLine(type, line); + } +} diff --git a/src/be/nikiroo/fanfix/output/LaTeX.java b/src/be/nikiroo/fanfix/output/LaTeX.java new file mode 100644 index 00000000..a8d6d379 --- /dev/null +++ b/src/be/nikiroo/fanfix/output/LaTeX.java @@ -0,0 +1,182 @@ +package be.nikiroo.fanfix.output; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.data.Paragraph.ParagraphType; + +class LaTeX extends BasicOutput { + protected FileWriter writer; + private boolean lastWasQuote = false; + + // quote chars + private char openQuote = Instance.getTrans().getChar( + StringId.OPEN_SINGLE_QUOTE); + private char closeQuote = Instance.getTrans().getChar( + StringId.CLOSE_SINGLE_QUOTE); + private char openDoubleQuote = Instance.getTrans().getChar( + StringId.OPEN_DOUBLE_QUOTE); + private char closeDoubleQuote = Instance.getTrans().getChar( + StringId.CLOSE_DOUBLE_QUOTE); + + @Override + public File process(Story story, File targetDir, String targetName) + throws IOException { + String targetNameOrig = targetName; + targetName += getDefaultExtension(); + + File target = new File(targetDir, targetName); + + writer = new FileWriter(target); + try { + super.process(story, targetDir, targetNameOrig); + } finally { + writer.close(); + writer = null; + } + + return target; + } + + @Override + protected String getDefaultExtension() { + return ".tex"; + } + + @Override + protected void writeStoryHeader(Story story) throws IOException { + String date = ""; + String author = ""; + String title = "\\title{}"; + String lang = ""; + if (story.getMeta() != null) { + MetaData meta = story.getMeta(); + title = "\\title{" + latexEncode(meta.getTitle()) + "}"; + date = "\\date{" + latexEncode(meta.getDate()) + "}"; + author = "\\author{" + latexEncode(meta.getAuthor()) + "}"; + lang = meta.getLang().toLowerCase(); + if (lang != null && !lang.isEmpty()) { + lang = Instance.getConfig().getStringX(Config.LATEX_LANG, lang); + if (lang == null) { + System.err.println(Instance.getTrans().getString( + StringId.LATEX_LANG_UNKNOWN, lang)); + } + } + } + + writer.append("%\n"); + writer.append("% This LaTeX document was auto-generated by Fanfic Reader, created by Niki.\n"); + writer.append("%\n\n"); + writer.append("\\documentclass[a4paper]{book}\n"); + if (lang != null && !lang.isEmpty()) { + writer.append("\\usepackage[" + lang + "]{babel}\n"); + } + writer.append("\\usepackage[utf8]{inputenc}\n"); + writer.append("\\usepackage[T1]{fontenc}\n"); + writer.append("\\usepackage{lmodern}\n"); + writer.append("\\newcommand{\\br}{\\vspace{10 mm}}\n"); + writer.append("\\newcommand{\\say}{--- \\noindent\\emph}\n"); + writer.append("\\hyphenpenalty=1000\n"); + writer.append("\\tolerance=5000\n"); + writer.append("\\begin{document}\n"); + if (story.getMeta() != null && story.getMeta().getDate() != null) + writer.append(date + "\n"); + writer.append(title + "\n"); + writer.append(author + "\n"); + writer.append("\\maketitle\n"); + writer.append("\n"); + + // TODO: cover + } + + @Override + protected void writeStoryFooter(Story story) throws IOException { + writer.append("\\end{document}\n"); + } + + @Override + protected void writeChapterHeader(Chapter chap) throws IOException { + writer.append("\n\n\\chapter{" + latexEncode(chap.getName()) + "}" + + "\n"); + } + + @Override + protected void writeChapterFooter(Chapter chap) throws IOException { + writer.write("\n"); + } + + @Override + protected String enbold(String word) { + return "\\textsc{" + word + "}"; + } + + @Override + protected String italize(String word) { + return "\\emph{" + word + "}"; + } + + @Override + protected void writeTextLine(ParagraphType type, String line) + throws IOException { + + line = decorateText(latexEncode(line)); + + switch (type) { + case BLANK: + writer.write("\n"); + lastWasQuote = false; + break; + case BREAK: + writer.write("\n\\br"); + writer.write("\n"); + lastWasQuote = false; + break; + case NORMAL: + writer.write(line); + writer.write("\n"); + lastWasQuote = false; + break; + case QUOTE: + writer.write("\n\\say{" + line + "}\n"); + if (lastWasQuote) { + writer.write("\n\\noindent{}"); + } + lastWasQuote = true; + break; + case IMAGE: + // TODO + break; + } + } + + private String latexEncode(String input) { + StringBuilder builder = new StringBuilder(); + for (char car : input.toCharArray()) { + // TODO: check restricted chars? + if (car == '^' || car == '$' || car == '\\' || car == '#' + || car == '%') { + builder.append('\\'); + builder.append(car); + } else if (car == openQuote) { + builder.append('`'); + } else if (car == closeQuote) { + builder.append('\''); + } else if (car == openDoubleQuote) { + builder.append("``"); + } else if (car == closeDoubleQuote) { + builder.append("''"); + } else { + builder.append(car); + } + } + + return builder.toString(); + } +} diff --git a/src/be/nikiroo/fanfix/output/Sysout.java b/src/be/nikiroo/fanfix/output/Sysout.java new file mode 100644 index 00000000..f6cd789c --- /dev/null +++ b/src/be/nikiroo/fanfix/output/Sysout.java @@ -0,0 +1,22 @@ +package be.nikiroo.fanfix.output; + +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; + +class Sysout extends BasicOutput { + @Override + protected void writeStoryHeader(Story story) { + System.out.println(story); + } + + @Override + protected void writeChapterHeader(Chapter chap) { + System.out.println(chap); + } + + @Override + protected void writeParagraphHeader(Paragraph para) { + System.out.println(para); + } +} diff --git a/src/be/nikiroo/fanfix/output/Text.java b/src/be/nikiroo/fanfix/output/Text.java new file mode 100644 index 00000000..22056ed3 --- /dev/null +++ b/src/be/nikiroo/fanfix/output/Text.java @@ -0,0 +1,126 @@ +package be.nikiroo.fanfix.output; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URL; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.data.Paragraph.ParagraphType; + +class Text extends BasicOutput { + protected FileWriter writer; + protected File targetDir; + + @Override + public File process(Story story, File targetDir, String targetName) + throws IOException { + String targetNameOrig = targetName; + targetName += getDefaultExtension(); + + this.targetDir = targetDir; + + File target = new File(targetDir, targetName); + + writer = new FileWriter(target); + try { + super.process(story, targetDir, targetNameOrig); + } finally { + writer.close(); + writer = null; + } + + return target; + } + + @Override + protected String getDefaultExtension() { + return ".txt"; + } + + @Override + protected void writeStoryHeader(Story story) throws IOException { + String title = ""; + String author = null; + String date = null; + + MetaData meta = story.getMeta(); + if (meta != null) { + title = meta.getTitle() == null ? "" : meta.getTitle(); + author = meta.getAuthor(); + date = meta.getDate(); + } + + writer.write(title); + writer.write("\n"); + if (author != null && !author.isEmpty()) { + writer.write("©" + author); + } + if (date != null && !date.isEmpty()) { + writer.write(" ("); + writer.write(date); + writer.write(")"); + } + writer.write("\n"); + + // resume: + if (meta != null && meta.getResume() != null) { + writeChapter(meta.getResume()); + } + } + + @Override + protected void writeChapterHeader(Chapter chap) throws IOException { + String txt; + if (chap.getName() != null && !chap.getName().isEmpty()) { + txt = Instance.getTrans().getString(StringId.CHAPTER_NAMED, + chap.getNumber(), chap.getName()); + } else { + txt = Instance.getTrans().getString(StringId.CHAPTER_UNNAMED, + chap.getNumber()); + } + + writer.write("\n" + txt + "\n"); + for (int i = 0; i < txt.length(); i++) { + writer.write("—"); + } + writer.write("\n\n"); + } + + @Override + protected void writeParagraphFooter(Paragraph para) throws IOException { + writer.write("\n"); + } + + @Override + protected void writeParagraphHeader(Paragraph para) throws IOException { + if (para.getType() == ParagraphType.IMAGE) { + File file = new File(targetDir, getCurrentImageBestName(true)); + Instance.getCache().saveAsImage(new URL(para.getContent()), file); + } + } + + @Override + protected void writeTextLine(ParagraphType type, String line) + throws IOException { + switch (type) { + case BLANK: + break; + case BREAK: + writer.write("\n* * *\n"); + break; + case NORMAL: + case QUOTE: + writer.write(line); + break; + case IMAGE: + writer.write("[" + getCurrentImageBestName(true) + "]"); + break; + } + } +} diff --git a/src/be/nikiroo/fanfix/output/epub.style.css b/src/be/nikiroo/fanfix/output/epub.style.css new file mode 100644 index 00000000..3999b9ca --- /dev/null +++ b/src/be/nikiroo/fanfix/output/epub.style.css @@ -0,0 +1,103 @@ +html { + text-align: justify; +} + +.titlepage { + padding-left: 10%; + padding-right: 10%; + width: 80%; +} + +h1 { + padding-bottom: 0; + margin-bottom: 0; + text-align: left; +} + +.type { + position: relative; + font-size: large; + color: #666666; + font-weight: bold; + padding-bottom: 10px; + text-align: left; +} + +.cover, .page-image { + width: 100%; +} + +.cover img { + height: 45%; + max-width: 100%; + margin: auto; +} + +.author { + text-align: right; + font-size: large; + font-style: italic; +} + +.book, .chapter_content { + text-indent: 40px; + padding-top: 40px; + padding-left: 5%; + padding-right: 5%; + width: 90%; +} + +h2 { + border: 1px solid black; + color: #222222; + padding-left: 10px; + padding-right: 10px; + display: block; + padding-bottom: 0; + margin-bottom: 0; +} + +h2 .chap { + color: #000000; + font-size: large; + font-variant: small-caps; + display: block; +} + +h2 .chap:first-letter { + font-weight: bold; +} + +h2 .chapnumber { + color: #000000; + font-size: xx-large; +} + +h2 .chaptitle { + color: #444444; + font-size: large; + font-style: italic; + padding-bottom: 5px; + text-align: right; + display: block; +} + +.normals { + /* padding-bottom: 20px; */ + +} + +.normal { + /* padding-bottom: 20px; */ + +} + +.dialogues { + /* padding-top: 10px; + padding-bottom: 10px; */ + +} + +.dialogue { + font-style: italic; +} \ No newline at end of file diff --git a/src/be/nikiroo/fanfix/output/package-info.java b/src/be/nikiroo/fanfix/output/package-info.java new file mode 100644 index 00000000..6b7e490e --- /dev/null +++ b/src/be/nikiroo/fanfix/output/package-info.java @@ -0,0 +1,12 @@ +/** + * This package contains all the output processors. + *

+ * Of those, only {@link be.nikiroo.fanfix.output.BasicOutput} is public, + * but it contains a method + * ({@link be.nikiroo.fanfix.output.BasicOutput#getOutput(be.nikiroo.fanfix.output.BasicOutput.OutputType, boolean)}) + * to get all the other + * {@link be.nikiroo.fanfix.output.BasicOutput.OutputType}s. + * + * @author niki + */ +package be.nikiroo.fanfix.output; \ No newline at end of file diff --git a/src/be/nikiroo/fanfix/package-info.java b/src/be/nikiroo/fanfix/package-info.java new file mode 100644 index 00000000..e1043395 --- /dev/null +++ b/src/be/nikiroo/fanfix/package-info.java @@ -0,0 +1,10 @@ +/** + * Fanfic Reader is a program that can support a few different websites from + * which to retrieve stories, then process them into epub (or other) + * files that you can read anywhere. + *

+ * It has support for a {@link be.nikiroo.fanfix.Library} system, too. + * + * @author niki + */ +package be.nikiroo.fanfix; \ No newline at end of file diff --git a/src/be/nikiroo/fanfix/reader/CliReader.java b/src/be/nikiroo/fanfix/reader/CliReader.java new file mode 100644 index 00000000..52a5ea43 --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/CliReader.java @@ -0,0 +1,146 @@ +package be.nikiroo.fanfix.reader; + +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.Library; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.output.BasicOutput.OutputType; +import be.nikiroo.fanfix.supported.BasicSupport; +import be.nikiroo.fanfix.supported.BasicSupport.SupportType; + +/** + * Command line {@link Story} reader. + *

+ * Will output stories to the console. + * + * @author niki + */ +public class CliReader { + private Story story; + + /** + * Create a new {@link CliReader} for a {@link Story} in the {@link Library} + * . + * + * @param luid + * the {@link Story} ID + * @throws IOException + * in case of I/O error + */ + public CliReader(String luid) throws IOException { + story = Instance.getLibrary().getStory(luid); + if (story == null) { + throw new IOException("Cannot retrieve story from library: " + luid); + } + } + + /** + * Create a new {@link CliReader} for an external {@link Story}. + * + * @param source + * the {@link Story} {@link URL} + * @throws IOException + * in case of I/O error + */ + public CliReader(URL source) throws IOException { + BasicSupport support = BasicSupport.getSupport(source); + if (support == null) { + throw new IOException("URL not supported: " + source.toString()); + } + + story = support.process(source); + if (story == null) { + throw new IOException( + "Cannot retrieve story from external source: " + + source.toString()); + + } + } + + /** + * Read the information about the {@link Story}. + */ + public void read() { + String title = ""; + String author = ""; + + MetaData meta = story.getMeta(); + if (meta != null) { + if (meta.getTitle() != null) { + title = meta.getTitle(); + } + + if (meta.getAuthor() != null) { + author = "©" + meta.getAuthor(); + if (meta.getDate() != null && !meta.getDate().isEmpty()) { + author = author + " (" + meta.getDate() + ")"; + } + } + } + + System.out.println(title); + System.out.println(author); + System.out.println(""); + + for (Chapter chap : story) { + if (chap.getName() != null && !chap.getName().isEmpty()) { + System.out.println(Instance.getTrans().getString( + StringId.CHAPTER_NAMED, chap.getNumber(), + chap.getName())); + } else { + System.out.println(Instance.getTrans().getString( + StringId.CHAPTER_UNNAMED, chap.getNumber())); + } + } + } + + /** + * Read the selected chapter (starting at 1). + * + * @param chapter + * the chapter + */ + public void read(int chapter) { + if (chapter > story.getChapters().size()) { + System.err.println("Chapter " + chapter + ": no such chapter"); + } else { + Chapter chap = story.getChapters().get(chapter - 1); + System.out.println("Chapter " + chap.getNumber() + ": " + + chap.getName()); + + for (Paragraph para : chap) { + System.out.println(para.getContent()); + System.out.println(""); + } + } + } + + /** + * List all the stories available in the {@link Library} by + * {@link OutputType} (or all of them if the given type is NULL) + * + * @param type + * the {@link OutputType} or NULL for all stories + */ + public static void list(SupportType type) { + List stories; + stories = Instance.getLibrary().getList(type); + + for (MetaData story : stories) { + String author = ""; + if (story.getAuthor() != null && !story.getAuthor().isEmpty()) { + author = " (" + story.getAuthor() + ")"; + } + + System.out.println(story.getLuid() + ": " + story.getTitle() + + author); + } + } +} diff --git a/src/be/nikiroo/fanfix/supported/BasicSupport.java b/src/be/nikiroo/fanfix/supported/BasicSupport.java new file mode 100644 index 00000000..74f11156 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/BasicSupport.java @@ -0,0 +1,1292 @@ +package be.nikiroo.fanfix.supported; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Scanner; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.StringId; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.data.Paragraph.ParagraphType; +import be.nikiroo.utils.StringUtils; + +/** + * This class is the base class used by the other support classes. It can be + * used outside of this package, and have static method that you can use to get + * access to the correct support class. + *

+ * It will be used with 'resources' (usually web pages or files). + * + * @author niki + */ +public abstract class BasicSupport { + /** + * The supported input types for which we can get a {@link BasicSupport} + * object. + * + * @author niki + */ + public enum SupportType { + /** EPUB files created with this program */ + EPUB, + /** Pure text file with some rules */ + TEXT, + /** TEXT but with associated .info file */ + INFO_TEXT, + /** My Little Pony fanfictions */ + FIMFICTION, + /** Fanfictions from a lot of different universes */ + FANFICTION, + /** Website with lots of Mangas */ + MANGAFOX, + /** Furry website with comics support */ + E621, + /** CBZ files */ + CBZ; + + /** + * A description of this support type (more information than the + * {@link BasicSupport#getSourceName()}). + * + * @return the description + */ + public String getDesc() { + String desc = Instance.getTrans().getStringX(StringId.INPUT_DESC, + this.name()); + + if (desc == null) { + desc = Instance.getTrans().getString(StringId.INPUT_DESC, this); + } + + return desc; + } + + /** + * The name of this support type (a short version). + * + * @return the name + */ + public String getSourceName() { + BasicSupport support = BasicSupport.getSupport(this); + if (support != null) { + return support.getSourceName(); + } + + return null; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + /** + * Call {@link SupportType#valueOf(String.toUpperCase())}. + * + * @param typeName + * the possible type name + * + * @return NULL or the type + */ + public static SupportType valueOfUC(String typeName) { + return SupportType.valueOf(typeName == null ? null : typeName + .toUpperCase()); + } + + /** + * Call {@link SupportType#valueOf(String.toUpperCase())} but return + * NULL for NULL instead of raising exception. + * + * @param typeName + * the possible type name + * + * @return NULL or the type + */ + public static SupportType valueOfNullOkUC(String typeName) { + if (typeName == null) { + return null; + } + + return SupportType.valueOfUC(typeName); + } + + /** + * Call {@link SupportType#valueOf(String.toUpperCase())} but return + * NULL in case of error instead of raising an exception. + * + * @param typeName + * the possible type name + * + * @return NULL or the type + */ + public static SupportType valueOfAllOkUC(String typeName) { + try { + return SupportType.valueOfUC(typeName); + } catch (Exception e) { + return null; + } + } + } + + /** Only used by {@link BasicSupport#getInput()} just so it is always reset. */ + private InputStream in; + private SupportType type; + private URL currentReferer; // with on 'r', as in 'HTTP'... + + // quote chars + private char openQuote = Instance.getTrans().getChar( + StringId.OPEN_SINGLE_QUOTE); + private char closeQuote = Instance.getTrans().getChar( + StringId.CLOSE_SINGLE_QUOTE); + private char openDoubleQuote = Instance.getTrans().getChar( + StringId.OPEN_DOUBLE_QUOTE); + private char closeDoubleQuote = Instance.getTrans().getChar( + StringId.CLOSE_DOUBLE_QUOTE); + + /** + * The name of this support class. + * + * @return the name + */ + protected abstract String getSourceName(); + + /** + * Check if the given resource is supported by this {@link BasicSupport}. + * + * @param url + * the resource to check for + * + * @return TRUE if it is + */ + protected abstract boolean supports(URL url); + + /** + * Return TRUE if the support will return HTML encoded content values for + * the chapters content. + * + * @return TRUE for HTML + */ + protected abstract boolean isHtml(); + + /** + * Return the story title. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the title + * + * @throws IOException + * in case of I/O error + */ + protected abstract String getTitle(URL source, InputStream in) + throws IOException; + + /** + * Return the story author. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the author + * + * @throws IOException + * in case of I/O error + */ + protected abstract String getAuthor(URL source, InputStream in) + throws IOException; + + /** + * Return the story publication date. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the date + * + * @throws IOException + * in case of I/O error + */ + protected abstract String getDate(URL source, InputStream in) + throws IOException; + + /** + * Return the subject of the story (for instance, if it is a fanfiction, + * what is the original work; if it is a technical text, what is the + * technical subject...). + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the subject + * + * @throws IOException + * in case of I/O error + */ + protected abstract String getSubject(URL source, InputStream in) + throws IOException; + + /** + * Return the story description. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the description + * + * @throws IOException + * in case of I/O error + */ + protected abstract String getDesc(URL source, InputStream in) + throws IOException; + + /** + * Return the story cover resource if any, or NULL if none. + *

+ * The default cover should not be checked for here. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the cover or NULL + * + * @throws IOException + * in case of I/O error + */ + protected abstract URL getCover(URL source, InputStream in) + throws IOException; + + /** + * Return the list of chapters (name and resource). + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the chapters + * + * @throws IOException + * in case of I/O error + */ + protected abstract List> getChapters(URL source, + InputStream in) throws IOException; + + /** + * Return the content of the chapter (possibly HTML encoded, if + * {@link BasicSupport#isHtml()} is TRUE). + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * @param number + * the chapter number + * + * @return the content + * + * @throws IOException + * in case of I/O error + */ + protected abstract String getChapterContent(URL source, InputStream in, + int number) throws IOException; + + /** + * Check if this {@link BasicSupport} is mainly catered to image files. + * + * @return TRUE if it is + */ + public boolean isImageDocument(URL source, InputStream in) + throws IOException { + return false; + } + + /** + * Return the list of cookies (values included) that must be used to + * correctly fetch the resources. + *

+ * You are expected to call the super method implementation if you override + * it. + * + * @return the cookies + */ + public Map getCookies() { + return new HashMap(); + } + + /** + * Process the given story resource into a partially filled {@link Story} + * object containing the name and metadata, except for the description. + * + * @param url + * the story resource + * + * @return the {@link Story} + * + * @throws IOException + * in case of I/O error + */ + public Story processMeta(URL url) throws IOException { + return processMeta(url, true, false); + } + + /** + * Process the given story resource into a partially filled {@link Story} + * object containing the name and metadata. + * + * @param url + * the story resource + * + * @param close + * close "this" and "in" when done + * + * @return the {@link Story} + * + * @throws IOException + * in case of I/O error + */ + protected Story processMeta(URL url, boolean close, boolean getDesc) + throws IOException { + in = Instance.getCache().open(url, this, false); + if (in == null) { + return null; + } + + try { + preprocess(getInput()); + + Story story = new Story(); + story.setMeta(new MetaData()); + story.getMeta().setTitle(ifUnhtml(getTitle(url, getInput()))); + story.getMeta().setAuthor( + fixAuthor(ifUnhtml(getAuthor(url, getInput())))); + story.getMeta().setDate(ifUnhtml(getDate(url, getInput()))); + story.getMeta().setTags(getTags(url, getInput())); + story.getMeta().setSource(getSourceName()); + story.getMeta().setPublisher( + ifUnhtml(getPublisher(url, getInput()))); + story.getMeta().setUuid(getUuid(url, getInput())); + story.getMeta().setLuid(getLuid(url, getInput())); + story.getMeta().setLang(getLang(url, getInput())); + story.getMeta().setSubject(ifUnhtml(getSubject(url, getInput()))); + story.getMeta().setImageDocument(isImageDocument(url, getInput())); + + if (getDesc) { + String descChapterName = Instance.getTrans().getString( + StringId.DESCRIPTION); + story.getMeta().setResume( + makeChapter(url, 0, descChapterName, + getDesc(url, getInput()))); + } + + return story; + } finally { + if (close) { + try { + close(); + } catch (IOException e) { + Instance.syserr(e); + } + + if (in != null) { + in.close(); + } + } + } + } + + /** + * Process the given story resource into a fully filled {@link Story} + * object. + * + * @param url + * the story resource + * + * @return the {@link Story} + * + * @throws IOException + * in case of I/O error + */ + public Story process(URL url) throws IOException { + setCurrentReferer(url); + + try { + Story story = processMeta(url, false, true); + if (story == null) { + return null; + } + + story.setChapters(new ArrayList()); + + URL cover = getCover(url, getInput()); + if (cover == null) { + String subject = story.getMeta() == null ? null : story + .getMeta().getSubject(); + if (subject != null && !subject.isEmpty() + && Instance.getCoverDir() != null) { + File fileCover = new File(Instance.getCoverDir(), subject); + cover = getImage(fileCover.toURI().toURL(), subject); + } + } + + if (cover != null) { + InputStream coverIn = null; + try { + coverIn = Instance.getCache().open(cover, this, true); + story.getMeta().setCover(StringUtils.toImage(coverIn)); + } catch (IOException e) { + Instance.syserr(new IOException(Instance.getTrans() + .getString(StringId.ERR_BS_NO_COVER, cover), e)); + } finally { + if (coverIn != null) + coverIn.close(); + } + } + + List> chapters = getChapters(url, getInput()); + int i = 1; + if (chapters != null) { + for (Entry chap : chapters) { + setCurrentReferer(chap.getValue()); + InputStream chapIn = Instance.getCache().open( + chap.getValue(), this, true); + try { + story.getChapters().add( + makeChapter(url, i, chap.getKey(), + getChapterContent(url, chapIn, i))); + } finally { + chapIn.close(); + } + i++; + } + } + + return story; + + } finally { + try { + close(); + } catch (IOException e) { + Instance.syserr(e); + } + + if (in != null) { + in.close(); + } + + currentReferer = null; + } + } + + /** + * The support type.$ + * + * @return the type + */ + public SupportType getType() { + return type; + } + + /** + * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e., + * the current {@link URL} we work on. + * + * @return the referer + */ + public URL getCurrentReferer() { + return currentReferer; + } + + /** + * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e., + * the current {@link URL} we work on. + * + * @param currentReferer + * the new referer + */ + protected void setCurrentReferer(URL currentReferer) { + this.currentReferer = currentReferer; + } + + /** + * The support type. + * + * @param type + * the new type + * + * @return this + */ + protected BasicSupport setType(SupportType type) { + this.type = type; + return this; + } + + /** + * Return the story publisher (by default, + * {@link BasicSupport#getSourceName()}). + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the publisher + * + * @throws IOException + * in case of I/O error + */ + protected String getPublisher(URL source, InputStream in) + throws IOException { + return getSourceName(); + } + + /** + * Return the story UUID, a unique value representing the story (it is often + * an URL). + *

+ * By default, this is the {@link URL} of the resource. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the uuid + * + * @throws IOException + * in case of I/O error + */ + protected String getUuid(URL source, InputStream in) throws IOException { + return source.toString(); + } + + /** + * Return the story Library UID, a unique value representing the story (it + * is often a number) in the local library. + *

+ * By default, this is empty. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the id + * + * @throws IOException + * in case of I/O error + */ + protected String getLuid(URL source, InputStream in) throws IOException { + return ""; + } + + /** + * Return the 2-letter language code of this story. + *

+ * By default, this is 'EN'. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the language + * + * @throws IOException + * in case of I/O error + */ + protected String getLang(URL source, InputStream in) throws IOException { + return "EN"; + } + + /** + * Return the list of tags for this story. + * + * @param source + * the source of the story + * @param in + * the input (the main resource) + * + * @return the tags + * + * @throws IOException + * in case of I/O error + */ + protected List getTags(URL source, InputStream in) + throws IOException { + return new ArrayList(); + } + + /** + * Return the first line from the given input which correspond to the given + * selectors. + *

+ * Do not reset the input, which will be pointing at the line just after the + * result (input will be spent if no result is found). + * + * @param in + * the input + * @param needle + * a string that must be found inside the target line + * @param relativeLine + * the line to return based upon the target line position (-1 = + * the line before, 0 = the target line...) + * + * @return the line + */ + protected String getLine(InputStream in, String needle, int relativeLine) { + return getLine(in, needle, relativeLine, true); + } + + /** + * Return a line from the given input which correspond to the given + * selectors. + *

+ * Do not reset the input, which will be pointing at the line just after the + * result (input will be spent if no result is found) when first is TRUE, + * and will always be spent if first is FALSE. + * + * @param in + * the input + * @param needle + * a string that must be found inside the target line + * @param relativeLine + * the line to return based upon the target line position (-1 = + * the line before, 0 = the target line...) + * @param first + * takes the first result (as opposed to the last one, which will + * also always spend the input) + * + * @return the line + */ + protected String getLine(InputStream in, String needle, int relativeLine, + boolean first) { + String rep = null; + + List lines = new ArrayList(); + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + int index = -1; + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + lines.add(scan.next()); + + if (index == -1 && lines.get(lines.size() - 1).contains(needle)) { + index = lines.size() - 1; + } + + if (index >= 0 && index + relativeLine < lines.size()) { + rep = lines.get(index + relativeLine); + if (first) { + break; + } + } + } + + return rep; + } + + /** + * Prepare the support if needed before processing. + * + * @throws IOException + * on I/O error + */ + protected void preprocess(InputStream in) throws IOException { + } + + /** + * Now that we have processed the {@link Story}, close the resources if any. + * + * @throws IOException + * on I/O error + */ + protected void close() throws IOException { + } + + /** + * Create a {@link Chapter} object from the given information, formatting + * the content as it should be. + * + * @param number + * the chapter number + * @param name + * the chapter name + * @param content + * the chapter content + * + * @return the {@link Chapter} + * + * @throws IOException + * in case of I/O error + */ + protected Chapter makeChapter(URL source, int number, String name, + String content) throws IOException { + + // Chapter name: process it correctly, then remove the possible + // redundant "Chapter x: " in front of it + String chapterName = processPara(name).getContent().trim(); + for (String lang : Instance.getConfig().getString(Config.CHAPTER) + .split(",")) { + String chapterWord = Instance.getConfig().getStringX( + Config.CHAPTER, lang); + if (chapterName.startsWith(chapterWord)) { + chapterName = chapterName.substring(chapterWord.length()) + .trim(); + break; + } + } + + if (chapterName.startsWith(Integer.toString(number))) { + chapterName = chapterName.substring( + Integer.toString(number).length()).trim(); + } + + if (chapterName.startsWith(":")) { + chapterName = chapterName.substring(1).trim(); + } + // + + Chapter chap = new Chapter(number, chapterName); + + if (content == null) { + return chap; + } + + if (isHtml()) { + // Special


processing: + content = content.replaceAll("(
]*>)|(
)|(
)", + "\n* * *\n"); + } + + InputStream in = new ByteArrayInputStream( + content.getBytes(StandardCharsets.UTF_8)); + try { + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("(\\n|

)"); // \n for test,

for html + + List paras = new ArrayList(); + while (scan.hasNext()) { + String line = scan.next().trim(); + boolean image = false; + if (line.startsWith("[") && line.endsWith("]")) { + URL url = getImage(source, + line.substring(1, line.length() - 1).trim()); + if (url != null) { + paras.add(new Paragraph(url)); + image = true; + } + } + + if (!image) { + paras.add(processPara(line)); + } + } + + // Check quotes for "bad" format + List newParas = new ArrayList(); + for (Paragraph para : paras) { + newParas.addAll(requotify(para)); + } + paras = newParas; + + // Remove double blanks/brks + boolean space = false; + boolean brk = true; + for (int i = 0; i < paras.size(); i++) { + Paragraph para = paras.get(i); + boolean thisSpace = para.getType() == ParagraphType.BLANK; + boolean thisBrk = para.getType() == ParagraphType.BREAK; + + if (space && thisBrk) { + paras.remove(i - 1); + i--; + } else if ((space || brk) && (thisSpace || thisBrk)) { + paras.remove(i); + i--; + } + + space = thisSpace; + brk = thisBrk; + } + + // Remove blank/brk at start + if (paras.size() > 0 + && (paras.get(0).getType() == ParagraphType.BLANK || paras + .get(0).getType() == ParagraphType.BREAK)) { + paras.remove(0); + } + + // Remove blank/brk at end + int last = paras.size() - 1; + if (paras.size() > 0 + && (paras.get(last).getType() == ParagraphType.BLANK || paras + .get(last).getType() == ParagraphType.BREAK)) { + paras.remove(last); + } + + chap.setParagraphs(paras); + + return chap; + } finally { + in.close(); + } + } + + /** + * Return the list of supported image extensions. + * + * @return the extensions + */ + protected String[] getImageExt(boolean emptyAllowed) { + if (emptyAllowed) { + return new String[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" }; + } else { + return new String[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" }; + } + } + + /** + * Check if the given resource can be a local image or a remote image, then + * refresh the cache with it if it is. + * + * @param source + * the story source + * @param line + * the resource to check + * + * @return the image URL if found, or NULL + * + */ + protected URL getImage(URL source, String line) { + String path = new File(source.getFile()).getParent(); + URL url = null; + + // try for files + try { + String urlBase = new File(new File(path), line.trim()).toURI() + .toURL().toString(); + for (String ext : getImageExt(true)) { + if (new File(urlBase + ext).exists()) { + url = new File(urlBase + ext).toURI().toURL(); + } + } + } catch (Exception e) { + // Nothing to do here + } + + if (url == null) { + // try for URLs + try { + for (String ext : getImageExt(true)) { + if (Instance.getCache().check(new URL(line + ext))) { + url = new URL(line + ext); + } + } + + // try out of cache + if (url == null) { + for (String ext : getImageExt(true)) { + try { + url = new URL(line + ext); + Instance.getCache().refresh(url, this, true); + break; + } catch (IOException e) { + // no image with this ext + url = null; + } + } + } + } catch (MalformedURLException e) { + // Not an url + } + } + + // refresh the cached file + if (url != null) { + try { + Instance.getCache().refresh(url, this, true); + } catch (IOException e) { + // woops, broken image + url = null; + } + } + + return url; + } + + /** + * Reset then return {@link BasicSupport#in}. + * + * @return {@link BasicSupport#in} + * + * @throws IOException + * in case of I/O error + */ + protected InputStream getInput() throws IOException { + in.reset(); + return in; + } + + /** + * Fix the author name if it is prefixed with some "by" {@link String}. + * + * @param author + * the author with a possible prefix + * + * @return the author without prefixes + */ + private String fixAuthor(String author) { + if (author != null) { + for (String suffix : new String[] { " ", ":" }) { + for (String byString : Instance.getConfig() + .getString(Config.BYS).split(",")) { + byString += suffix; + if (author.toUpperCase().startsWith(byString.toUpperCase())) { + author = author.substring(byString.length()).trim(); + } + } + } + + // Special case (without suffix): + if (author.startsWith("©")) { + author = author.substring(1); + } + } + + return author; + } + + /** + * Check quotes for bad format (i.e., quotes with normal paragraphs inside) + * and requotify them (i.e., separate them into QUOTE paragraphs and other + * paragraphs (quotes or not)). + * + * @param para + * the paragraph to requotify (not necessaraly a quote) + * + * @return the correctly (or so we hope) quotified paragraphs + */ + private List requotify(Paragraph para) { + List newParas = new ArrayList(); + + if (para.getType() == ParagraphType.QUOTE) { + String line = para.getContent(); + boolean singleQ = line.startsWith("" + openQuote); + boolean doubleQ = line.startsWith("" + openDoubleQuote); + + if (!singleQ && !doubleQ) { + line = openDoubleQuote + line + closeDoubleQuote; + newParas.add(new Paragraph(ParagraphType.QUOTE, line)); + } else { + char close = singleQ ? closeQuote : closeDoubleQuote; + int posClose = line.indexOf(close); + int posDot = line.indexOf("."); + while (posDot >= 0 && posDot < posClose) { + posDot = line.indexOf(".", posDot + 1); + } + + if (posDot >= 0) { + String rest = line.substring(posDot + 1).trim(); + line = line.substring(0, posDot + 1).trim(); + newParas.add(new Paragraph(ParagraphType.QUOTE, line)); + newParas.addAll(requotify(processPara(rest))); + } else { + newParas.add(para); + } + } + } else { + newParas.add(para); + } + + return newParas; + } + + /** + * Process a {@link Paragraph} from a raw line of text. + *

+ * Will also fix quotes and HTML encoding if needed. + * + * @param line + * the raw line + * + * @return the processed {@link Paragraph} + */ + private Paragraph processPara(String line) { + line = ifUnhtml(line).trim(); + + boolean space = true; + boolean brk = true; + boolean quote = false; + boolean tentativeCloseQuote = false; + char prev = '\0'; + int dashCount = 0; + + StringBuilder builder = new StringBuilder(); + for (char car : line.toCharArray()) { + if (car != '-') { + if (dashCount > 0) { + // dash, ndash and mdash: - – — + // currently: always use mdash + builder.append(dashCount == 1 ? '-' : '—'); + } + dashCount = 0; + } + + if (tentativeCloseQuote) { + tentativeCloseQuote = false; + if ((car >= 'a' && car <= 'z') || (car >= 'A' && car <= 'Z') + || (car >= '0' && car <= '9')) { + builder.append("'"); + } else { + builder.append(closeQuote); + } + } + + switch (car) { + case ' ': // note: unbreakable space + case ' ': + case '\t': + case '\n': // just in case + case '\r': // just in case + builder.append(' '); + break; + + case '\'': + if (space || (brk && quote)) { + quote = true; + builder.append(openQuote); + } else if (prev == ' ') { + builder.append(openQuote); + } else { + // it is a quote ("I'm off") or a 'quote' ("This + // 'good' restaurant"...) + tentativeCloseQuote = true; + } + break; + + case '"': + if (space || (brk && quote)) { + quote = true; + builder.append(openDoubleQuote); + } else if (prev == ' ') { + builder.append(openDoubleQuote); + } else { + builder.append(closeDoubleQuote); + } + break; + + case '-': + if (space) { + quote = true; + } else { + dashCount++; + } + space = false; + break; + + case '*': + case '~': + case '/': + case '\\': + case '<': + case '>': + case '=': + case '+': + case '_': + case '–': + case '—': + space = false; + builder.append(car); + break; + + case '‘': + case '`': + case '‹': + case '﹁': + case '〈': + case '「': + if (space || (brk && quote)) { + quote = true; + builder.append(openQuote); + } else { + builder.append(openQuote); + } + space = false; + brk = false; + break; + + case '’': + case '›': + case '﹂': + case '〉': + case '」': + space = false; + brk = false; + builder.append(closeQuote); + break; + + case '«': + case '“': + case '﹃': + case '《': + case '『': + if (space || (brk && quote)) { + quote = true; + builder.append(openDoubleQuote); + } else { + builder.append(openDoubleQuote); + } + space = false; + brk = false; + break; + + case '»': + case '”': + case '﹄': + case '》': + case '』': + space = false; + brk = false; + builder.append(closeDoubleQuote); + break; + + default: + space = false; + brk = false; + builder.append(car); + break; + } + + prev = car; + } + + if (tentativeCloseQuote) { + tentativeCloseQuote = false; + builder.append(closeQuote); + } + + line = builder.toString().trim(); + + ParagraphType type = ParagraphType.NORMAL; + if (space) { + type = ParagraphType.BLANK; + } else if (brk) { + type = ParagraphType.BREAK; + } else if (quote) { + type = ParagraphType.QUOTE; + } + + return new Paragraph(type, line); + } + + /** + * Remove the HTML from the inpit if {@link BasicSupport#isHtml()} is + * true. + * + * @param input + * the input + * + * @return the no html version if needed + */ + private String ifUnhtml(String input) { + if (isHtml() && input != null) { + return StringUtils.unhtml(input); + } + + return input; + } + + /** + * Return a {@link BasicSupport} implementation supporting the given + * resource if possible. + * + * @param url + * the story resource + * + * @return an implementation that supports it, or NULL + */ + public static BasicSupport getSupport(URL url) { + if (url == null) { + return null; + } + + // TEXT and INFO_TEXT always support files (not URLs though) + for (SupportType type : SupportType.values()) { + if (type != SupportType.TEXT && type != SupportType.INFO_TEXT) { + BasicSupport support = getSupport(type); + if (support != null && support.supports(url)) { + return support; + } + } + } + + for (SupportType type : new SupportType[] { SupportType.TEXT, + SupportType.INFO_TEXT }) { + BasicSupport support = getSupport(type); + if (support != null && support.supports(url)) { + return support; + } + } + + return null; + } + + /** + * Return a {@link BasicSupport} implementation supporting the given type. + * + * @param type + * the type + * + * @return an implementation that supports it, or NULL + */ + public static BasicSupport getSupport(SupportType type) { + switch (type) { + case EPUB: + return new Epub().setType(type); + case INFO_TEXT: + return new InfoText().setType(type); + case FIMFICTION: + return new Fimfiction().setType(type); + case FANFICTION: + return new Fanfiction().setType(type); + case TEXT: + return new Text().setType(type); + case MANGAFOX: + return new MangaFox().setType(type); + case E621: + return new E621().setType(type); + case CBZ: + return new Cbz().setType(type); + } + + return null; + } +} diff --git a/src/be/nikiroo/fanfix/supported/Cbz.java b/src/be/nikiroo/fanfix/supported/Cbz.java new file mode 100644 index 00000000..012c0474 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/Cbz.java @@ -0,0 +1,93 @@ +package be.nikiroo.fanfix.supported; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.Paragraph; +import be.nikiroo.fanfix.data.Story; + +/** + * Support class for CBZ files (works better with CBZ created with this program, + * as they have some metadata available). + * + * @author niki + */ +class Cbz extends Epub { + @Override + protected boolean supports(URL url) { + return url.toString().toLowerCase().endsWith(".cbz"); + } + + @Override + public String getSourceName() { + return "cbz"; + } + + @Override + protected String getDataPrefix() { + return ""; + } + + @Override + protected boolean requireInfo() { + return false; + } + + @Override + public boolean isImageDocument(URL source, InputStream in) + throws IOException { + return true; + } + + @Override + protected boolean getCover() { + return false; + } + + @Override + public Story process(URL url) throws IOException { + Story story = processMeta(url, false, true); + story.setChapters(new ArrayList()); + Chapter chap = new Chapter(1, null); + story.getChapters().add(chap); + + ZipInputStream zipIn = new ZipInputStream(getInput()); + + for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn + .getNextEntry()) { + if (!entry.isDirectory() + && entry.getName().startsWith(getDataPrefix())) { + String entryLName = entry.getName().toLowerCase(); + boolean imageEntry = false; + for (String ext : getImageExt(false)) { + if (entryLName.endsWith(ext)) { + imageEntry = true; + } + } + + if (imageEntry) { + try { + // we assume that we can get the UUID without a stream + String uuid = getUuid(url, null) + "_" + + entry.getName(); + + Instance.getCache().addToCache(zipIn, uuid); + chap.getParagraphs().add( + new Paragraph(new File(uuid).toURI().toURL())); + } catch (Exception e) { + Instance.syserr(e); + } + } + } + } + + return story; + } +} diff --git a/src/be/nikiroo/fanfix/supported/E621.java b/src/be/nikiroo/fanfix/supported/E621.java new file mode 100644 index 00000000..2455c875 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/E621.java @@ -0,0 +1,257 @@ +package be.nikiroo.fanfix.supported; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Scanner; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.Chapter; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.utils.StringUtils; + +/** + * Support class for e621.net and e926.net, a Furry website supporting comics, + * including some of MLP. + *

+ * e926.net only shows the "clean" images and + * comics, but it can be difficult to browse. + * + * @author niki + */ +class E621 extends BasicSupport { + @Override + public String getSourceName() { + return "e621.net"; + } + + @Override + public boolean isImageDocument(URL source, InputStream in) { + return true; + } + + @Override + public Story process(URL url) throws IOException { + // There is no chapters on e621, just pagination... + Story story = super.process(url); + + Chapter only = new Chapter(1, null); + for (Chapter chap : story) { + only.getParagraphs().addAll(chap.getParagraphs()); + } + + story.getChapters().clear(); + story.getChapters().add(only); + + return story; + } + + @Override + protected boolean supports(URL url) { + String host = url.getHost(); + if (host.startsWith("www.")) { + host = host.substring("www.".length()); + } + + return ("e621.net".equals(host) || "e926.net".equals(host)) + && url.getPath().startsWith("/pool/"); + } + + @Override + protected boolean isHtml() { + return true; + } + + @Override + protected String getAuthor(URL source, InputStream in) throws IOException { + String author = getLine(in, "href=\"/post/show/", 0); + if (author != null) { + String key = "href=\""; + int pos = author.indexOf(key); + if (pos >= 0) { + author = author.substring(pos + key.length()); + pos = author.indexOf("\""); + if (pos >= 0) { + author = author.substring(0, pos - 1); + String page = source.getProtocol() + "://" + + source.getHost() + author; + InputStream pageIn = Instance.getCache().open( + new URL(page), this, false); + try { + key = "class=\"tag-type-artist\""; + author = getLine(pageIn, key, 0); + if (author != null) { + pos = author.indexOf("= 0) { + author = author.substring(pos); + pos = author.indexOf(""); + if (pos >= 0) { + author = author.substring(0, pos); + return StringUtils.unhtml(author); + } + } + } + } finally { + pageIn.close(); + } + } + } + } + + return null; + } + + @Override + protected String getDate(URL source, InputStream in) throws IOException { + return null; + } + + @Override + protected String getSubject(URL source, InputStream in) throws IOException { + return null; + } + + @Override + protected URL getCover(URL source, InputStream in) throws IOException { + return null; + } + + @Override + protected String getTitle(URL source, InputStream in) throws IOException { + String title = getLine(in, "", 0); + if (title != null) { + int pos = title.indexOf('>'); + if (pos >= 0) { + title = title.substring(pos + 1); + pos = title.indexOf('<'); + if (pos >= 0) { + title = title.substring(0, pos); + } + } + + if (title.startsWith("Pool:")) { + title = title.substring("Pool:".length()); + } + + title = title.trim(); + } + + return title; + } + + @Override + protected String getDesc(URL source, InputStream in) throws IOException { + String desc = getLine(in, "margin-bottom: 2em;", 0); + + if (desc != null) { + StringBuilder builder = new StringBuilder(); + + boolean inTags = false; + for (char car : desc.toCharArray()) { + if ((inTags && car == '>') || (!inTags && car == '<')) { + inTags = !inTags; + } + + if (inTags) { + builder.append(car); + } + } + + return builder.toString().trim(); + } + + return null; + } + + @Override + protected List<Entry<String, URL>> getChapters(URL source, InputStream in) + throws IOException { + List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>(); + int last = 1; // no pool/show when only one page + + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + for (int pos = line.indexOf(source.getPath()); pos >= 0; pos = line + .indexOf(source.getPath(), pos + source.getPath().length())) { + int equalPos = line.indexOf("=", pos); + int quotePos = line.indexOf("\"", pos); + if (equalPos >= 0 && quotePos > equalPos) { + String snum = line.substring(equalPos + 1, quotePos); + try { + int num = Integer.parseInt(snum); + if (num > last) { + last = num; + } + } catch (NumberFormatException e) { + } + } + } + } + + for (int i = 1; i <= last; i++) { + final String key = Integer.toString(i); + final URL value = new URL(source.toString() + "?page=" + i); + urls.add(new Entry<String, URL>() { + public URL setValue(URL value) { + return null; + } + + public URL getValue() { + return value; + } + + public String getKey() { + return key; + } + }); + } + + return urls; + } + + @Override + protected String getChapterContent(URL source, InputStream in, int number) + throws IOException { + StringBuilder builder = new StringBuilder(); + String staticSite = "https://static1.e621.net"; + if (source.getHost().contains("e926")) { + staticSite = staticSite.replace("e621", "e926"); + } + + String key = staticSite + "/data/preview/"; + + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + if (line.contains("class=\"preview\"")) { + for (int pos = line.indexOf(key); pos >= 0; pos = line.indexOf( + key, pos + key.length())) { + int endPos = line.indexOf("\"", pos); + if (endPos >= 0) { + String id = line.substring(pos + key.length(), endPos); + id = staticSite + "/data/" + id; + + int dotPos = id.lastIndexOf("."); + if (dotPos >= 0) { + id = id.substring(0, dotPos); + builder.append("["); + builder.append(id); + builder.append("]\n"); + } + } + } + } + } + + return builder.toString(); + } +} diff --git a/src/be/nikiroo/fanfix/supported/Epub.java b/src/be/nikiroo/fanfix/supported/Epub.java new file mode 100644 index 00000000..31bf7257 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/Epub.java @@ -0,0 +1,293 @@ +package be.nikiroo.fanfix.supported; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.Map.Entry; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.imageio.ImageIO; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.utils.IOUtils; +import be.nikiroo.utils.MarkableFileInputStream; + +/** + * Support class for EPUB files created with this program (as we need some + * metadata available in those we create). + * + * @author niki + */ +class Epub extends BasicSupport { + private InfoText base; + private URL fakeSource; + + private File tmpCover; + private File tmpInfo; + private File tmp; + + /** Only used by {@link Epub#getInput()} so it is always reset. */ + private InputStream in; + + @Override + public String getSourceName() { + return "epub"; + } + + @Override + protected boolean supports(URL url) { + if (url.getPath().toLowerCase().endsWith(".epub")) { + return true; + } + + return false; + } + + @Override + protected boolean isHtml() { + if (tmpInfo.exists()) { + return base.isHtml(); + } + + return false; + } + + @Override + protected String getTitle(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getTitle(fakeSource, getFakeInput()); + } + + return source.toString(); + } + + @Override + protected String getAuthor(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getAuthor(fakeSource, getFakeInput()); + } + + return null; + } + + @Override + protected String getDate(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getDate(fakeSource, getFakeInput()); + } + + return null; + } + + @Override + protected String getSubject(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getSubject(fakeSource, getFakeInput()); + } + + return null; + } + + @Override + protected String getDesc(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getDesc(fakeSource, getFakeInput()); + } + + return null; + } + + @Override + protected URL getCover(URL source, InputStream in) throws IOException { + if (tmpCover.exists()) { + return tmpCover.toURI().toURL(); + } + + return null; + } + + @Override + protected List<Entry<String, URL>> getChapters(URL source, InputStream in) + throws IOException { + if (tmpInfo.exists()) { + return base.getChapters(fakeSource, getFakeInput()); + } + + return null; + } + + @Override + protected String getChapterContent(URL source, InputStream in, int number) + throws IOException { + if (tmpInfo.exists()) { + return base.getChapterContent(fakeSource, getFakeInput(), number); + } + + return null; + } + + @Override + protected String getLang(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getLang(fakeSource, getFakeInput()); + } + + return super.getLang(source, in); + } + + @Override + protected String getPublisher(URL source, InputStream in) + throws IOException { + if (tmpInfo.exists()) { + return base.getPublisher(fakeSource, getFakeInput()); + } + + return super.getPublisher(source, in); + } + + @Override + protected List<String> getTags(URL source, InputStream in) + throws IOException { + if (tmpInfo.exists()) { + return base.getTags(fakeSource, getFakeInput()); + } + + return super.getTags(source, in); + } + + @Override + protected String getUuid(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getUuid(fakeSource, getFakeInput()); + } + + return super.getUuid(source, in); + } + + @Override + protected String getLuid(URL source, InputStream in) throws IOException { + if (tmpInfo.exists()) { + return base.getLuid(fakeSource, getFakeInput()); + } + + return super.getLuid(source, in); + } + + @Override + protected void preprocess(InputStream in) throws IOException { + // Note: do NOT close this stream, as it would also close "in" + ZipInputStream zipIn = new ZipInputStream(in); + tmp = File.createTempFile("fanfic-reader-parser_", ".tmp"); + tmpInfo = new File(tmp + ".info"); + tmpCover = File.createTempFile("fanfic-reader-parser_", ".tmp"); + + base = new InfoText(); + fakeSource = tmp.toURI().toURL(); + + for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn + .getNextEntry()) { + if (!entry.isDirectory() + && entry.getName().startsWith(getDataPrefix())) { + String entryLName = entry.getName().toLowerCase(); + boolean imageEntry = false; + for (String ext : getImageExt(false)) { + if (entryLName.endsWith(ext)) { + imageEntry = true; + } + } + + if (entry.getName().equals(getDataPrefix() + "version")) { + // Nothing to do for now ("first" + // version is 3.0) + } else if (entryLName.endsWith(".info")) { + // Info file + IOUtils.write(zipIn, tmpInfo); + } else if (imageEntry) { + // Cover + if (getCover()) { + try { + BufferedImage image = ImageIO.read(zipIn); + ImageIO.write(image, Instance.getConfig() + .getString(Config.IMAGE_FORMAT_COVER) + .toLowerCase(), tmpCover); + } catch (Exception e) { + Instance.syserr(e); + } + } + } else if (entry.getName().equals(getDataPrefix() + "URL")) { + // Do nothing + } else if (entry.getName().equals(getDataPrefix() + "SUMMARY")) { + // Do nothing + } else { + // Hopefully the data file + IOUtils.write(zipIn, tmp); + } + } + } + + if (requireInfo() && (!tmp.exists() || !tmpInfo.exists())) { + throw new IOException( + "file not supported (maybe not created with this program or corrupt)"); + } + + if (tmp.exists()) { + this.in = new MarkableFileInputStream(new FileInputStream(tmp)); + } + } + + @Override + protected void close() throws IOException { + for (File file : new File[] { tmp, tmpInfo, tmpCover }) { + if (file != null && file.exists()) { + if (!file.delete()) { + file.deleteOnExit(); + } + } + } + + tmp = null; + tmpInfo = null; + tmpCover = null; + fakeSource = null; + + try { + if (in != null) { + in.close(); + } + } finally { + in = null; + base.close(); + } + } + + protected String getDataPrefix() { + return "DATA/"; + } + + protected boolean requireInfo() { + return true; + } + + protected boolean getCover() { + return true; + } + + /** + * Reset then return {@link Epub#in}. + * + * @return {@link Epub#in} + * + * @throws IOException + * in case of I/O error + */ + private InputStream getFakeInput() throws IOException { + in.reset(); + return in; + } +} diff --git a/src/be/nikiroo/fanfix/supported/Fanfiction.java b/src/be/nikiroo/fanfix/supported/Fanfiction.java new file mode 100644 index 00000000..cbbc0851 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/Fanfiction.java @@ -0,0 +1,289 @@ +package be.nikiroo.fanfix.supported; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map.Entry; +import java.util.Scanner; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.utils.StringUtils; + +/** + * Support class for <a href="http://www.fanfiction.net/">Faniction.net</a> + * stories, a website dedicated to fanfictions of many, many different + * universes, from TV shows to novels to games. + * + * @author niki + */ +class Fanfiction extends BasicSupport { + @Override + protected boolean isHtml() { + return true; + } + + @Override + public String getSourceName() { + return "Fanfiction.net"; + } + + @Override + protected String getSubject(URL source, InputStream in) { + String line = getLine(in, "id=pre_story_links", 0); + if (line != null) { + int pos = line.lastIndexOf('"'); + if (pos >= 1) { + line = line.substring(pos + 1); + pos = line.indexOf('<'); + if (pos >= 0) { + return line.substring(0, pos); + } + } + } + + return null; + } + + @Override + protected List<String> getTags(URL source, InputStream in) + throws IOException { + List<String> tags = super.getTags(source, in); + + String key = "title=\"Send Private Message\""; + String line = getLine(in, key, 2); + if (line != null) { + key = "Rated:"; + int pos = line.indexOf(key); + if (pos >= 0) { + line = line.substring(pos + key.length()); + key = "Chapters:"; + pos = line.indexOf(key); + if (pos >= 0) { + line = line.substring(0, pos); + line = StringUtils.unhtml(line).trim(); + if (line.endsWith("-")) { + line = line.substring(0, line.length() - 1); + } + + for (String tag : line.split("-")) { + tags.add(tag.trim()); + } + } + } + } + + return tags; + } + + @Override + protected String getTitle(URL source, InputStream in) { + int i = 0; + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + if (line.contains("xcontrast_txt")) { + if ((++i) == 2) { + line = StringUtils.unhtml(line).trim(); + if (line.startsWith("Follow/Fav")) { + line = line.substring("Follow/Fav".length()).trim(); + } + + return line; + } + } + } + + return null; + } + + @Override + protected String getAuthor(URL source, InputStream in) { + int i = 0; + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + if (line.contains("xcontrast_txt")) { + if ((++i) == 3) { + return StringUtils.unhtml(line).trim(); + } + } + } + + return null; + } + + @Override + protected String getDate(URL source, InputStream in) { + String key = "Published: <span data-xutime='"; + String line = getLine(in, key, 0); + if (line != null) { + int pos = line.indexOf(key); + if (pos >= 0) { + line = line.substring(pos + key.length()); + pos = line.indexOf('\''); + if (pos >= 0) { + line = line.substring(0, pos).trim(); + try { + SimpleDateFormat sdf = new SimpleDateFormat( + "YYYY-MM-dd"); + return sdf + .format(new Date(1000 * Long.parseLong(line))); + } catch (NumberFormatException e) { + Instance.syserr(new IOException( + "Cannot convert publication date: " + line, e)); + } + } + } + } + + return null; + } + + @Override + protected String getDesc(URL source, InputStream in) { + return getLine(in, "title=\"Send Private Message\"", 1); + } + + @Override + protected URL getCover(URL url, InputStream in) { + String key = "class='cimage"; + String line = getLine(in, key, 0); + if (line != null) { + int pos = line.indexOf(key); + if (pos >= 0) { + line = line.substring(pos + key.length()); + key = "src='"; + pos = line.indexOf(key); + if (pos >= 0) { + line = line.substring(pos + key.length()); + pos = line.indexOf('\''); + if (pos >= 0) { + line = line.substring(0, pos); + if (line.startsWith("//")) { + line = url.getProtocol() + "://" + + line.substring(2); + } else if (line.startsWith("//")) { + line = url.getProtocol() + "://" + url.getHost() + + "/" + line.substring(1); + } else { + line = url.getProtocol() + "://" + url.getHost() + + "/" + url.getPath() + "/" + line; + } + + try { + return new URL(line); + } catch (MalformedURLException e) { + Instance.syserr(e); + } + } + } + } + } + + return null; + } + + @Override + protected List<Entry<String, URL>> getChapters(URL source, InputStream in) { + List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>(); + + String base = source.toString(); + int pos = base.lastIndexOf('/'); + String suffix = base.substring(pos); // including '/' at start + base = base.substring(0, pos); + if (base.endsWith("/1")) { + base = base.substring(0, base.length() - 1); // including '/' at end + } + + String line = getLine(in, "id=chap_select", 0); + String key = "<option value="; + int i = 1; + for (pos = line.indexOf(key); pos >= 0; pos = line.indexOf(key, pos), i++) { + pos = line.indexOf('>', pos); + if (pos >= 0) { + int endOfName = line.indexOf('<', pos); + if (endOfName >= 0) { + String name = line.substring(pos + 1, endOfName); + String chapNum = i + "."; + if (name.startsWith(chapNum)) { + name = name.substring(chapNum.length(), name.length()); + } + + try { + final String chapName = name.trim(); + final URL chapURL = new URL(base + i + suffix); + urls.add(new Entry<String, URL>() { + public URL setValue(URL value) { + return null; + } + + public URL getValue() { + return chapURL; + } + + public String getKey() { + return chapName; + } + }); + } catch (MalformedURLException e) { + Instance.syserr(new IOException("Cannot parse chapter " + + i + " url: " + (base + i + suffix), e)); + } + } + } + } + + return urls; + } + + @Override + protected String getChapterContent(URL source, InputStream in, int number) { + StringBuilder builder = new StringBuilder(); + String startAt = "class='storytext "; + String endAt1 = "function review_init"; + String endAt2 = "id=chap_select"; + boolean ok = false; + + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + if (!ok && line.contains(startAt)) { + ok = true; + } else if (ok && (line.contains(endAt1) || line.contains(endAt2))) { + ok = false; + break; + } + + if (ok) { + // First line may contain the title and chap name again + if (builder.length() == 0) { + int pos = line.indexOf("<hr"); + if (pos >= 0) { + line = line.substring(pos); + } + } + + builder.append(line); + } + } + + return builder.toString(); + } + + @Override + protected boolean supports(URL url) { + return "fanfiction.net".equals(url.getHost()) + || "www.fanfiction.net".equals(url.getHost()); + } +} diff --git a/src/be/nikiroo/fanfix/supported/Fimfiction.java b/src/be/nikiroo/fanfix/supported/Fimfiction.java new file mode 100644 index 00000000..61f61d2f --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/Fimfiction.java @@ -0,0 +1,236 @@ +package be.nikiroo.fanfix.supported; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Scanner; + +import be.nikiroo.fanfix.Instance; + +/** + * Support class for <a href="http://www.fimfiction.net/">FimFiction.net</a> + * stories, a website dedicated to My Little Pony. + * + * @author niki + */ +class Fimfiction extends BasicSupport { + @Override + protected boolean isHtml() { + return true; + } + + @Override + public String getSourceName() { + return "FimFiction.net"; + } + + @Override + protected String getSubject(URL source, InputStream in) { + return "MLP"; + } + + @Override + public Map<String, String> getCookies() { + Map<String, String> cookies = new HashMap<String, String>(); + cookies.put("view_mature", "true"); + return cookies; + } + + @Override + protected List<String> getTags(URL source, InputStream in) { + List<String> tags = new ArrayList<String>(); + tags.add("MLP"); + + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + if (line.contains("story_category") && !line.contains("title=")) { + int pos = line.indexOf('>'); + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('<'); + if (pos >= 0) { + line = line.substring(0, pos); + } + } + + line = line.trim(); + if (!tags.contains(line)) { + tags.add(line); + } + } + } + + return tags; + } + + @Override + protected String getTitle(URL source, InputStream in) { + String line = getLine(in, " property=\"og:title\"", 0); + if (line != null) { + int pos = -1; + for (int i = 0; i < 3; i++) { + pos = line.indexOf('"', pos + 1); + } + + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('"'); + if (pos >= 0) { + return line.substring(0, pos); + } + } + } + + return null; + } + + @Override + protected String getAuthor(URL source, InputStream in) { + String line = getLine(in, " href=\"/user/", 0); + if (line != null) { + int pos = line.indexOf('"'); + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('"'); + if (pos >= 0) { + line = line.substring(0, pos); + pos = line.lastIndexOf('/'); + if (pos >= 0) { + line = line.substring(pos + 1); + return line.replace('+', ' '); + } + } + } + } + + return null; + } + + @Override + protected String getDate(URL source, InputStream in) { + String line = getLine(in, "<span class=\"date\">", 0); + if (line != null) { + int pos = -1; + for (int i = 0; i < 3; i++) { + pos = line.indexOf('>', pos + 1); + } + + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('<'); + if (pos >= 0) { + return line.substring(0, pos).trim(); + } + } + } + + return null; + } + + @Override + protected String getDesc(URL source, InputStream in) { + // the og: meta version is the SHORT resume, this is the LONG resume + return getLine(in, "class=\"more_button hidden\"", -1); + } + + @Override + protected URL getCover(URL url, InputStream in) { + // Note: the 'og:image' is the SMALL cover, not the full version + String cover = getLine(in, "<div class=\"story_image\">", 1); + if (cover != null) { + int pos = cover.indexOf('"'); + if (pos >= 0) { + cover = cover.substring(pos + 1); + pos = cover.indexOf('"'); + if (pos >= 0) { + cover = cover.substring(0, pos); + } + } + } + + if (cover != null) { + try { + return new URL(cover); + } catch (MalformedURLException e) { + Instance.syserr(e); + } + } + + return null; + } + + @Override + protected List<Entry<String, URL>> getChapters(URL source, InputStream in) { + List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>(); + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + if (line.contains("class=\"chapter_link\"") + || line.contains("class='chapter_link'")) { + // Chapter name + String name = line; + int pos = name.indexOf('>'); + if (pos >= 0) { + name = name.substring(pos + 1); + pos = name.indexOf('<'); + if (pos >= 0) { + name = name.substring(0, pos); + } + } + // Chapter content + pos = line.indexOf('/'); + if (pos >= 0) { + line = line.substring(pos); // we take the /, not +1 + pos = line.indexOf('"'); + if (pos >= 0) { + line = line.substring(0, pos); + } + } + + try { + final String key = name; + final URL value = new URL("http://www.fimfiction.net" + + line); + urls.add(new Entry<String, URL>() { + public URL setValue(URL value) { + return null; + } + + public String getKey() { + return key; + } + + public URL getValue() { + return value; + } + }); + } catch (MalformedURLException e) { + Instance.syserr(e); + } + } + } + + return urls; + } + + @Override + protected String getChapterContent(URL source, InputStream in, int number) { + return getLine(in, "<div id=\"chapter_container\">", 1); + } + + @Override + protected boolean supports(URL url) { + return "fimfiction.net".equals(url.getHost()) + || "www.fimfiction.net".equals(url.getHost()); + } +} diff --git a/src/be/nikiroo/fanfix/supported/InfoText.java b/src/be/nikiroo/fanfix/supported/InfoText.java new file mode 100644 index 00000000..a627714a --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/InfoText.java @@ -0,0 +1,248 @@ +package be.nikiroo.fanfix.supported; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + +import be.nikiroo.fanfix.Instance; + +/** + * Support class for <tt>.info</tt> text files ({@link Text} files with a + * <tt>.info</tt> metadata file next to them). + * <p> + * The <tt>.info</tt> file is supposed to be written by this program, or + * compatible. + * + * @author niki + */ +class InfoText extends Text { + @Override + public String getSourceName() { + return "info-text"; + } + + @Override + protected String getTitle(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "TITLE"); + if (tag != null) { + return tag; + } + + return super.getTitle(source, in); + } + + @Override + protected String getAuthor(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "AUTHOR"); + if (tag != null) { + return tag; + } + + return super.getAuthor(source, in); + } + + @Override + protected String getDate(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "DATE"); + if (tag != null) { + return tag; + } + + return super.getDate(source, in); + } + + @Override + protected String getSubject(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "SUBJECT"); + if (tag != null) { + return tag; + } + + return super.getSubject(source, in); + } + + @Override + protected String getLang(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "LANG"); + if (tag != null) { + return tag; + } + + return super.getLang(source, in); + } + + @Override + protected String getPublisher(URL source, InputStream in) + throws IOException { + String tag = getInfoTag(source, "PUBLISHER"); + if (tag != null) { + return tag; + } + + return super.getPublisher(source, in); + } + + @Override + protected String getUuid(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "UUID"); + if (tag != null) { + return tag; + } + + return super.getUuid(source, in); + } + + @Override + protected String getLuid(URL source, InputStream in) throws IOException { + String tag = getInfoTag(source, "LUID"); + if (tag != null) { + return tag; + } + + return super.getLuid(source, in); + } + + @Override + protected List<String> getTags(URL source, InputStream in) + throws IOException { + List<String> tags = super.getTags(source, in); + + String tt = getInfoTag(source, "TAGS"); + if (tt != null) { + for (String tag : tt.split(",")) { + tags.add(tag.trim()); + } + } + + return tags; + } + + @Override + public boolean isImageDocument(URL source, InputStream in) + throws IOException { + String tag = getInfoTag(source, "IMAGES_DOCUMENT"); + if (tag != null) { + return tag.trim().toLowerCase().equals("true"); + } + + return super.isImageDocument(source, in); + } + + @Override + protected URL getCover(URL source, InputStream in) { + File file; + try { + file = new File(source.toURI()); + file = new File(file.getPath() + ".info"); + } catch (URISyntaxException e) { + Instance.syserr(e); + file = null; + } + + String path = null; + if (file != null && file.exists()) { + try { + InputStream infoIn = new FileInputStream(file); + try { + String key = "COVER="; + String tt = getLine(infoIn, key, 0); + if (tt != null && !tt.isEmpty()) { + tt = tt.substring(key.length()).trim(); + if (tt.startsWith("'") && tt.endsWith("'")) { + tt = tt.substring(1, tt.length() - 1).trim(); + } + + URL cover = getImage(source, tt); + if (cover != null) { + path = cover.getFile(); + } + } + } finally { + infoIn.close(); + } + } catch (MalformedURLException e) { + Instance.syserr(e); + } catch (IOException e) { + Instance.syserr(e); + } + } + + if (path != null) { + try { + return new File(path).toURI().toURL(); + } catch (MalformedURLException e) { + Instance.syserr(e); + } + } + + return null; + } + + @Override + protected boolean supports(URL url) { + if ("file".equals(url.getProtocol())) { + File file; + try { + file = new File(url.toURI()); + file = new File(file.getPath() + ".info"); + } catch (URISyntaxException e) { + Instance.syserr(e); + file = null; + } + + return file != null && file.exists(); + } + + return false; + } + + /** + * Return the value of the given tag in the <tt>.info</tt> file if present. + * + * @param source + * the source story {@link URL} + * @param key + * the tag key + * + * @return the value or NULL + * + * @throws IOException + * in case of I/O error + */ + private String getInfoTag(URL source, String key) throws IOException { + key += "="; + + File file; + try { + file = new File(source.toURI()); + file = new File(file.getPath() + ".info"); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + if (file.exists()) { + InputStream infoIn = new FileInputStream(file); + try { + String value = getLine(infoIn, key, 0); + if (value != null && !value.isEmpty()) { + value = value.trim().substring(key.length()).trim(); + if (value.startsWith("'") && value.endsWith("'") + || value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1).trim(); + } + + return value; + } + } finally { + infoIn.close(); + } + } + + return null; + } +} diff --git a/src/be/nikiroo/fanfix/supported/MangaFox.java b/src/be/nikiroo/fanfix/supported/MangaFox.java new file mode 100644 index 00000000..fb72bf53 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/MangaFox.java @@ -0,0 +1,409 @@ +package be.nikiroo.fanfix.supported; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; +import java.util.Scanner; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.utils.StringUtils; + +class MangaFox extends BasicSupport { + @Override + protected boolean isHtml() { + return true; + } + + @Override + public String getSourceName() { + return "MangaFox.met"; + } + + @Override + protected String getSubject(URL source, InputStream in) { + return "manga"; + } + + @Override + public boolean isImageDocument(URL source, InputStream in) + throws IOException { + return true; + } + + @Override + protected List<String> getTags(URL source, InputStream in) { + List<String> tags = new ArrayList<String>(); + + String line = getLine(in, "/genres/", 0); + if (line != null) { + line = StringUtils.unhtml(line); + String[] tab = line.split(","); + if (tab != null) { + for (String tag : tab) { + tags.add(tag.trim()); + } + } + } + + return tags; + } + + @Override + protected String getTitle(URL source, InputStream in) { + String line = getLine(in, " property=\"og:title\"", 0); + if (line != null) { + int pos = -1; + for (int i = 0; i < 3; i++) { + pos = line.indexOf('"', pos + 1); + } + + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('"'); + if (pos >= 0) { + return line.substring(0, pos); + } + } + } + + return null; + } + + @Override + protected String getAuthor(URL source, InputStream in) { + List<String> authors = new ArrayList<String>(); + + String line = getLine(in, "/author/", 0, false); + if (line != null) { + for (String ln : StringUtils.unhtml(line).split(",")) { + if (ln != null && !ln.trim().isEmpty() + && !authors.contains(ln.trim())) { + authors.add(ln.trim()); + } + } + } + + try { + in.reset(); + } catch (IOException e) { + Instance.syserr(e); + } + + line = getLine(in, "/artist/", 0, false); + if (line != null) { + for (String ln : StringUtils.unhtml(line).split(",")) { + if (ln != null && !ln.trim().isEmpty() + && !authors.contains(ln.trim())) { + authors.add(ln.trim()); + } + } + } + + if (authors.isEmpty()) { + return null; + } else { + StringBuilder builder = new StringBuilder(); + for (String author : authors) { + if (builder.length() > 0) { + builder.append(", "); + } + + builder.append(author); + } + + return builder.toString(); + } + } + + @Override + protected String getDate(URL source, InputStream in) { + String line = getLine(in, "/released/", 0); + if (line != null) { + line = StringUtils.unhtml(line); + return line.trim(); + } + + return null; + } + + @Override + protected String getDesc(URL source, InputStream in) { + String line = getLine(in, " property=\"og:description\"", 0); + if (line != null) { + int pos = -1; + for (int i = 0; i < 3; i++) { + pos = line.indexOf('"', pos + 1); + } + + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('"'); + if (pos >= 0) { + return line.substring(0, pos); + } + } + } + + return null; + } + + @Override + protected URL getCover(URL url, InputStream in) { + String line = getLine(in, " property=\"og:image\"", 0); + String cover = null; + if (line != null) { + int pos = -1; + for (int i = 0; i < 3; i++) { + pos = line.indexOf('"', pos + 1); + } + + if (pos >= 0) { + line = line.substring(pos + 1); + pos = line.indexOf('"'); + if (pos >= 0) { + cover = line.substring(0, pos); + } + } + } + + if (cover != null) { + try { + return new URL(cover); + } catch (MalformedURLException e) { + Instance.syserr(e); + } + } + + return null; + } + + @Override + protected List<Entry<String, URL>> getChapters(URL source, InputStream in) { + List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>(); + + String volumeAt = "<h3 class=\"volume\">"; + String linkAt = "href=\"http://mangafox.me/"; + String endAt = "<script type=\"text/javascript\">"; + + boolean started = false; + + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + while (scan.hasNext()) { + String line = scan.next(); + + if (started && line.contains(endAt)) { + break; + } else if (!started && line.contains(volumeAt)) { + started = true; + } + + if (started && line.contains(linkAt)) { + // Chapter content url + String url = null; + int pos = line.indexOf("href=\""); + if (pos >= 0) { + line = line.substring(pos + "href=\"".length()); + pos = line.indexOf('\"'); + if (pos >= 0) { + url = line.substring(0, pos); + } + } + + // Chapter name + String name = null; + if (scan.hasNext()) { + name = StringUtils.unhtml(scan.next()).trim(); + // Remove the "new" tag if present + if (name.endsWith("new")) { + name = name.substring(0, name.length() - 3).trim(); + } + } + + // to help with the retry and the originalUrl + refresh(url); + + try { + final String key = name; + final URL value = new URL(url); + urls.add(new Entry<String, URL>() { + public URL setValue(URL value) { + return null; + } + + public String getKey() { + return key; + } + + public URL getValue() { + return value; + } + }); + } catch (MalformedURLException e) { + Instance.syserr(e); + } + } + } + + // the chapters are in reversed order + Collections.reverse(urls); + + return urls; + } + + @Override + protected String getChapterContent(URL source, InputStream in, int number) { + StringBuilder builder = new StringBuilder(); + String base = getCurrentReferer().toString(); + int pos = base.lastIndexOf('/'); + base = base.substring(0, pos + 1); // including the '/' at the end + + boolean close = false; + while (in != null) { + String linkNextLine = getLine(in, "return enlarge()", 0); + try { + in.reset(); + } catch (IOException e) { + Instance.syserr(e); + } + + String linkImageLine = getLine(in, "return enlarge()", 1); + String linkNext = null; + String linkImage = null; + pos = linkNextLine.indexOf("href=\""); + if (pos >= 0) { + linkNextLine = linkNextLine.substring(pos + "href=\"".length()); + pos = linkNextLine.indexOf('\"'); + if (pos >= 0) { + linkNext = linkNextLine.substring(0, pos); + } + } + pos = linkImageLine.indexOf("src=\""); + if (pos >= 0) { + linkImageLine = linkImageLine + .substring(pos + "src=\"".length()); + pos = linkImageLine.indexOf('\"'); + if (pos >= 0) { + linkImage = linkImageLine.substring(0, pos); + } + } + + if (linkImage != null) { + builder.append("["); + // to help with the retry and the originalUrl, part 1 + builder.append(withoutQuery(linkImage)); + builder.append("]\n"); + } + + // to help with the retry and the originalUrl, part 2 + refresh(linkImage); + + if (close) { + try { + in.close(); + } catch (IOException e) { + Instance.syserr(e); + } + } + + in = null; + if (linkNext != null && !"javascript:void(0);".equals(linkNext)) { + URL url; + try { + url = new URL(base + linkNext); + in = openEx(base + linkNext); + setCurrentReferer(url); + } catch (IOException e) { + Instance.syserr(new IOException( + "Cannot get the next manga page which is: " + + linkNext, e)); + } + } + + close = true; + } + + setCurrentReferer(source); + return builder.toString(); + } + + @Override + protected boolean supports(URL url) { + return "mangafox.me".equals(url.getHost()) + || "www.mangafox.me".equals(url.getHost()); + } + + /** + * Refresh the {@link URL} by calling {@link MangaFox#openEx(String)}. + * + * @param url + * the URL to refresh + * + * @return TRUE if it was refreshed + */ + private boolean refresh(String url) { + try { + openEx(url).close(); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Open the URL through the cache, but: retry a second time after 100ms if + * it fails, remove the query part of the {@link URL} before saving it to + * the cache (so it can be recalled later). + * + * @param url + * the {@link URL} + * + * @return the resource + * + * @throws IOException + * in case of I/O error + */ + private InputStream openEx(String url) throws IOException { + try { + return Instance.getCache().open(new URL(url), this, true, + withoutQuery(url)); + } catch (Exception e) { + // second chance + try { + Thread.sleep(100); + } catch (InterruptedException ee) { + } + + return Instance.getCache().open(new URL(url), this, true, + withoutQuery(url)); + } + } + + /** + * Return the same input {@link URL} but without the query part. + * + * @param url + * the inpiut {@link URL} as a {@link String} + * + * @return the input {@link URL} without query + */ + private URL withoutQuery(String url) { + URL o = null; + try { + // Remove the query from o (originalUrl), so it can be cached + // correctly + o = new URL(url); + o = new URL(o.getProtocol() + "://" + o.getHost() + o.getPath()); + + return o; + } catch (MalformedURLException e) { + return null; + } + } +} diff --git a/src/be/nikiroo/fanfix/supported/Text.java b/src/be/nikiroo/fanfix/supported/Text.java new file mode 100644 index 00000000..f1ee71c1 --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/Text.java @@ -0,0 +1,295 @@ +package be.nikiroo.fanfix.supported; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Scanner; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; + +/** + * Support class for local stories encoded in textual format, with a few rules: + * <ul> + * <li>The title must be on the first line</li> + * <li>The author (preceded by nothing, "by " or "©") must be on the second + * line, possibly with the publication date in parenthesis (i.e., " + * <tt>By Unknown (3rd October 1998)</tt>")</li> + * <li>Chapters must be declared with "<tt>Chapter x</tt>" or " + * <tt>Chapter x: NAME OF THE CHAPTER</tt>", where "<tt>x</tt>" is the chapter + * number</li> + * <li>A description of the story must be given as chapter number 0</li> + * <li>A cover may be present, with the same filename but a PNG, JPEG or JPG + * extension</li< + * </ul> + * + * @author niki + */ +class Text extends BasicSupport { + @Override + protected boolean isHtml() { + return false; + } + + @Override + public String getSourceName() { + return "text"; + } + + @Override + protected String getPublisher(URL source, InputStream in) + throws IOException { + return ""; + } + + @Override + protected String getSubject(URL source, InputStream in) throws IOException { + try { + File file = new File(source.toURI()); + return file.getParentFile().getName(); + } catch (URISyntaxException e) { + throw new IOException("Cannot parse the URL to File: " + + source.toString(), e); + } + + } + + @Override + protected String getLang(URL source, InputStream in) throws IOException { + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + scan.next(); // Title + scan.next(); // Author (Date) + String chapter0 = scan.next(); // empty or Chapter 0 + while (chapter0.isEmpty()) { + chapter0 = scan.next(); + } + + String lang = detectChapter(chapter0); + if (lang == null) { + lang = super.getLang(source, in); + } else { + lang = lang.toUpperCase(); + } + + return lang; + } + + @Override + protected String getTitle(URL source, InputStream in) throws IOException { + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + return scan.next(); + } + + @Override + protected String getAuthor(URL source, InputStream in) throws IOException { + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + scan.next(); + String authorDate = scan.next(); + + String author = authorDate; + int pos = authorDate.indexOf('('); + if (pos >= 0) { + author = authorDate.substring(0, pos); + } + + return author; + } + + @Override + protected String getDate(URL source, InputStream in) throws IOException { + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + scan.next(); + String authorDate = scan.next(); + + String date = ""; + int pos = authorDate.indexOf('('); + if (pos >= 0) { + date = authorDate.substring(pos + 1).trim(); + pos = date.lastIndexOf(')'); + if (pos >= 0) { + date = date.substring(0, pos).trim(); + } + } + + return date; + } + + @Override + protected String getDesc(URL source, InputStream in) { + return getChapterContent(source, in, 0); + } + + @Override + protected URL getCover(URL source, InputStream in) { + String path; + try { + path = new File(source.toURI()).getPath(); + } catch (URISyntaxException e) { + Instance.syserr(e); + path = null; + } + + for (String ext : new String[] { ".txt", ".text", ".story" }) { + if (path.endsWith(ext)) { + path = path.substring(0, path.length() - ext.length()); + } + } + + return getImage(source, path); + } + + @Override + protected List<Entry<String, URL>> getChapters(URL source, InputStream in) { + List<Entry<String, URL>> chaps = new ArrayList<Entry<String, URL>>(); + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + boolean descSkipped = false; + boolean prevLineEmpty = false; + while (scan.hasNext()) { + String line = scan.next(); + if (prevLineEmpty && detectChapter(line) != null) { + if (descSkipped) { + String chapName = Integer.toString(chaps.size()); + int pos = line.indexOf(':'); + if (pos >= 0 && pos + 1 < line.length()) { + chapName = line.substring(pos + 1).trim(); + } + final URL value = source; + final String key = chapName; + chaps.add(new Entry<String, URL>() { + public URL setValue(URL value) { + return null; + } + + public URL getValue() { + return value; + } + + public String getKey() { + return key; + } + }); + } else { + descSkipped = true; + } + } + + prevLineEmpty = line.trim().isEmpty(); + } + + return chaps; + } + + @Override + protected String getChapterContent(URL source, InputStream in, int number) { + StringBuilder builder = new StringBuilder(); + @SuppressWarnings("resource") + Scanner scan = new Scanner(in, "UTF-8"); + scan.useDelimiter("\\n"); + boolean inChap = false; + boolean prevLineEmpty = false; + while (scan.hasNext()) { + String line = scan.next(); + if (prevLineEmpty) { + if (detectChapter(line, number) != null) { + inChap = true; + } else if (inChap) { + if (prevLineEmpty && detectChapter(line) != null) { + break; + } + + builder.append(line); + builder.append("\n"); + } + } + + prevLineEmpty = line.trim().isEmpty(); + } + + return builder.toString(); + } + + @Override + protected boolean supports(URL url) { + if ("file".equals(url.getProtocol())) { + File file; + try { + file = new File(url.toURI()); + file = new File(file.getPath() + ".info"); + } catch (URISyntaxException e) { + Instance.syserr(e); + file = null; + } + + return file == null || !file.exists(); + } + + return false; + } + + /** + * Check if the given line looks like a starting chapter in a supported + * language, and return the language if it does (or NULL if not). + * + * @param line + * the line to check + * + * @return the language or NULL + */ + private String detectChapter(String line) { + return detectChapter(line, null); + } + + /** + * Check if the given line looks like the given starting chapter in a + * supported language, and return the language if it does (or NULL if not). + * + * @param line + * the line to check + * + * @return the language or NULL + */ + private String detectChapter(String line, Integer number) { + line = line.toUpperCase(); + for (String lang : Instance.getConfig().getString(Config.CHAPTER) + .split(",")) { + String chapter = Instance.getConfig().getStringX(Config.CHAPTER, + lang); + if (chapter != null && !chapter.isEmpty()) { + chapter = chapter.toUpperCase() + " "; + if (line.startsWith(chapter)) { + if (number != null) { + // We want "[CHAPTER] [number]: [name]", with ": [name]" + // optional + String test = line.substring(chapter.length()).trim(); + if (test.startsWith(Integer.toString(number))) { + test = test.substring( + Integer.toString(number).length()).trim(); + if (test.isEmpty() || test.startsWith(":")) { + return lang; + } + } + } else { + return lang; + } + } + } + } + + return null; + } +} diff --git a/src/be/nikiroo/fanfix/supported/package-info.java b/src/be/nikiroo/fanfix/supported/package-info.java new file mode 100644 index 00000000..1762e32e --- /dev/null +++ b/src/be/nikiroo/fanfix/supported/package-info.java @@ -0,0 +1,11 @@ +/** + * This package contains different implementation of + * {@link be.nikiroo.fanfix.supported.BasicSupport} to cater to different + * sources. + * <p> + * You are expected to use the static methods from + * {@link be.nikiroo.fanfix.supported.BasicSupport} to get those you need. + * + * @author niki + */ +package be.nikiroo.fanfix.supported; \ No newline at end of file -- 2.27.0