X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Ffanfix%2Fsupported%2FBasicSupport.java;h=56a3bb80cb6d51fe40a9c4d830e12cb275102d76;hb=d11fb35b34e44744c8b1f9226321f133af4eb151;hp=74f11156e82c265a0eee1ddbd09b7c417a98ed12;hpb=08fe2e33007063e30fe22dc1d290f8afaa18eb1d;p=fanfix.git diff --git a/src/be/nikiroo/fanfix/supported/BasicSupport.java b/src/be/nikiroo/fanfix/supported/BasicSupport.java index 74f1115..56a3bb8 100644 --- a/src/be/nikiroo/fanfix/supported/BasicSupport.java +++ b/src/be/nikiroo/fanfix/supported/BasicSupport.java @@ -1,27 +1,30 @@ 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.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Scanner; +import java.util.Map.Entry; + +import org.json.JSONException; +import org.json.JSONObject; +import org.jsoup.helper.DataUtil; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; 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.Progress; import be.nikiroo.utils.StringUtils; /** @@ -34,135 +37,14 @@ import be.nikiroo.utils.StringUtils; * @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 Document sourceNode; + private URL source; 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(); + private URL currentReferer; // with only one 'r', as in 'HTTP'... + + static protected BasicSupportHelper bsHelper = new BasicSupportHelper(); + static protected BasicSupportImages bsImages = new BasicSupportImages(); + static protected BasicSupportPara bsPara = new BasicSupportPara(new BasicSupportHelper(), new BasicSupportImages()); /** * Check if the given resource is supported by this {@link BasicSupport}. @@ -183,149 +65,59 @@ public abstract class BasicSupport { 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. + * Return the {@link MetaData} of this story. * - * @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 + * @return the associated {@link MetaData}, never NULL * * @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; + protected abstract MetaData getMeta() 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; + protected abstract String getDesc() throws IOException; /** - * Return the story cover resource if any, or NULL if none. + * Return the list of chapters (name and resource). *
- * The default cover should not be checked for here.
+ * Can be NULL if this {@link BasicSupport} do no use chapters.
*
- * @param source
- * the source of the story
- * @param in
- * the input (the main resource)
+ * @param pg
+ * the optional progress reporter
*
- * @return the cover or NULL
+ * @return the chapters or NULL
*
* @throws IOException
* in case of I/O error
*/
- protected abstract URL getCover(URL source, InputStream in)
+ protected abstract List
- * By default, this is the {@link URL} of the resource.
+ * Can return NULL, in which case you are supposed to work without a source
+ * node.
*
* @param source
- * the source of the story
- * @param in
- * the input (the main resource)
+ * the source {@link URL}
*
- * @return the uuid
+ * @return the {@link InputStream}
*
* @throws IOException
* in case of I/O error
*/
- protected String getUuid(URL source, InputStream in) throws IOException {
- return source.toString();
+ protected Document loadDocument(URL source) throws IOException {
+ String url = getCanonicalUrl(source).toString();
+ return DataUtil.load(Instance.getInstance().getCache().open(source, this, false), "UTF-8", url.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
+ * Log into the support (can be a no-op depending upon the support).
*
* @throws IOException
* in case of I/O error
*/
- protected String getLuid(URL source, InputStream in) throws IOException {
- return "";
+ protected void login() throws IOException {
}
/**
- * 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
+ * Now that we have processed the {@link Story}, close the resources if any.
*/
- protected String getLang(URL source, InputStream in) throws IOException {
- return "EN";
+ protected void close() {
+ setCurrentReferer(null);
}
/**
- * Return the list of tags for this story.
+ * Process the given story resource into a partially filled {@link Story}
+ * object containing the name and metadata.
*
- * @param source
- * the source of the story
- * @param in
- * the input (the main resource)
+ * @param getDesc
+ * retrieve the description of the story, or not
+ * @param pg
+ * the optional progress reporter
*
- * @return the tags
+ * @return the {@link Story}, never NULL
*
* @throws IOException
* in case of I/O error
*/
- protected List
- * 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);
- }
+ if (pg == null) {
+ pg = new Progress();
+ } else {
+ pg.setMinMax(0, 100);
+ }
- /**
- * 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;
+ pg.setProgress(30);
- List
+ * Note that this method expects small JSON files (everything is copied into
+ * memory at least twice).
*
- * @param number
- * the chapter number
- * @param name
- * the chapter name
- * @param content
- * the chapter content
+ * @param url
+ * the URL to parse
+ * @param stable
+ * TRUE for more stable resources, FALSE when they often change
*
- * @return the {@link Chapter}
+ * @return the JSON object
*
* @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
+ * Note that this method expects small JSON files (everything is copied into
+ * memory at least twice).
*
- * @param source
- * the story source
- * @param line
- * the resource to check
+ * @param url
+ * the URL to parse
+ * @param stable
+ * TRUE for more stable resources, FALSE when they often change
*
- * @return the image URL if found, or NULL
+ * @return the JSON object
*
+ * @throws IOException
+ * in case of I/O error
*/
- protected URL getImage(URL source, String line) {
- String path = new File(source.getFile()).getParent();
- URL url = null;
-
- // try for files
+ protected JSONObject getJson(URL url, boolean stable) throws IOException {
+ InputStream in = Instance.getInstance().getCache().open(url, null,
+ stable);
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
+ Scanner scan = new Scanner(in);
+ scan.useDelimiter("\0");
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 new JSONObject(scan.next());
+ } catch (JSONException e) {
+ throw new IOException(e);
+ } finally {
+ scan.close();
}
+ } finally {
+ in.close();
}
-
- return url;
}
/**
- * Reset then return {@link BasicSupport#in}.
+ * Process the given story resource into a fully filled {@link Story}
+ * object.
*
- * @return {@link BasicSupport#in}
+ * @param pg
+ * the optional progress reporter
+ *
+ * @return the {@link Story}, never NULL
*
* @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();
- }
- }
- }
+ // TODO: ADD final when BasicSupport_Deprecated is gone
+ public Story process(Progress pg) throws IOException {
+ setCurrentReferer(source);
+ login();
+ sourceNode = loadDocument(source);
- // Special case (without suffix):
- if (author.startsWith("©")) {
- author = author.substring(1);
+ try {
+ Story story = doProcess(pg);
+
+ // Check for "no chapters" stories
+ if (story.getChapters().isEmpty()
+ && story.getMeta().getResume() != null
+ && !story.getMeta().getResume().getParagraphs().isEmpty()) {
+ Chapter resume = story.getMeta().getResume();
+ resume.setName("");
+ resume.setNumber(1);
+ story.getChapters().add(resume);
+ story.getMeta().setWords(resume.getWords());
+
+ String descChapterName = Instance.getInstance().getTrans()
+ .getString(StringId.DESCRIPTION);
+ resume = new Chapter(0, descChapterName);
+ story.getMeta().setResume(resume);
}
+
+ return story;
+ } finally {
+ close();
}
-
- 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)).
+ * Actual processing step, without the calls to other methods.
+ *
+ * Will convert the story resource into a fully filled {@link Story} object.
*
- * @param para
- * the paragraph to requotify (not necessaraly a quote)
+ * @param pg
+ * the optional progress reporter
*
- * @return the correctly (or so we hope) quotified paragraphs
+ * @return the {@link Story}, never NULL
+ *
+ * @throws IOException
+ * in case of I/O error
*/
- private List
- * 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);
+
+ pg.setName("Initialising");
+
+ pg.setProgress(1);
+ Progress pgMeta = new Progress();
+ pg.addProgress(pgMeta, 10);
+ Story story = processMeta(true, pgMeta);
+ pgMeta.done(); // 10%
+ pg.put("meta", story.getMeta());
+
+ Progress pgGetChapters = new Progress();
+ pg.addProgress(pgGetChapters, 10);
+ story.setChapters(new ArrayList
processing:
- content = content.replaceAll("(
]*>)|(
)|(
)",
- "\n* * *\n");
- }
-
- InputStream in = new ByteArrayInputStream(
- content.getBytes(StandardCharsets.UTF_8));
+ protected JSONObject getJson(String url, boolean stable)
+ throws IOException {
try {
- @SuppressWarnings("resource")
- Scanner scan = new Scanner(in, "UTF-8");
- scan.useDelimiter("(\\n|