import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
import be.nikiroo.fanfix.Instance;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.supported.SupportType;
import be.nikiroo.utils.Image;
import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.StringUtils;
/**
* Manage a library of Stories: import, export, list, modify.
/**
* Return the cover image associated to this source.
* <p>
- * By default, return the cover of the first story with this source.
+ * By default, return the custom cover if any, and if not, return the cover
+ * of the first story with this source.
*
* @param source
* the source
* @return the cover image or NULL
*/
public Image getSourceCover(String source) {
+ Image custom = getCustomSourceCover(source);
+ if (custom != null) {
+ return custom;
+ }
+
List<MetaData> metas = getListBySource(source);
if (metas.size() > 0) {
return getCover(metas.get(0).getLuid());
}
/**
- * Fix the source cover to the given story cover.
+ * Return the cover image associated to this author.
+ * <p>
+ * By default, return the custom cover if any, and if not, return the cover
+ * of the first story with this author.
+ *
+ * @param author
+ * the author
+ *
+ * @return the cover image or NULL
+ */
+ public Image getAuthorCover(String author) {
+ Image custom = getCustomAuthorCover(author);
+ if (custom != null) {
+ return custom;
+ }
+
+ List<MetaData> metas = getListByAuthor(author);
+ if (metas.size() > 0) {
+ return getCover(metas.get(0).getLuid());
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the custom cover image associated to this source.
+ * <p>
+ * By default, return NULL.
+ *
+ * @param source
+ * the source to look for
+ *
+ * @return the custom cover or NULL if none
+ */
+ public Image getCustomSourceCover(@SuppressWarnings("unused") String source) {
+ return null;
+ }
+
+ /**
+ * Return the custom cover image associated to this author.
+ * <p>
+ * By default, return NULL.
+ *
+ * @param author
+ * the author to look for
+ *
+ * @return the custom cover or NULL if none
+ */
+ public Image getCustomAuthorCover(@SuppressWarnings("unused") String author) {
+ return null;
+ }
+
+ /**
+ * Set the source cover to the given story cover.
*
* @param source
* the source to change
*/
public abstract void setSourceCover(String source, String luid);
+ /**
+ * Set the author cover to the given story cover.
+ *
+ * @param source
+ * the author to change
+ * @param luid
+ * the story LUID
+ */
+ public abstract void setAuthorCover(String author, String luid);
+
/**
* Return the list of stories (represented by their {@link MetaData}, which
* <b>MAY</b> not have the cover included).
* Invalidate the {@link Story} cache (when the content should be re-read
* because it was changed).
*/
- protected void deleteInfo() {
- deleteInfo(null);
+ protected void invalidateInfo() {
+ invalidateInfo(null);
}
/**
* the LUID of the {@link Story} to clear from the cache, or NULL
* for all stories
*/
- protected abstract void deleteInfo(String luid);
+ protected abstract void invalidateInfo(String luid);
/**
* Invalidate the {@link Story} cache (when the content has changed, but we
return list;
}
+ /**
+ * List all the known types (sources) of stories, grouped by directory
+ * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1").
+ * <p>
+ * Note that an empty item in the list means a non-grouped source (type) --
+ * e.g., you could have for Source_1:
+ * <ul>
+ * <li><tt></tt>: empty, so source is "Source_1"</li>
+ * <li><tt>a</tt>: empty, so source is "Source_1/a"</li>
+ * <li><tt>b</tt>: empty, so source is "Source_1/b"</li>
+ * </ul>
+ *
+ * @return the grouped list
+ */
+ public synchronized Map<String, List<String>> getSourcesGrouped() {
+ Map<String, List<String>> map = new TreeMap<String, List<String>>();
+ for (String source : getSources()) {
+ String name;
+ String subname;
+
+ int pos = source.indexOf('/');
+ if (pos > 0 && pos < source.length() - 1) {
+ name = source.substring(0, pos);
+ subname = source.substring(pos + 1);
+
+ } else {
+ name = source;
+ subname = "";
+ }
+
+ List<String> list = map.get(name);
+ if (list == null) {
+ list = new ArrayList<String>();
+ map.put(name, list);
+ }
+ list.add(subname);
+ }
+
+ return map;
+ }
+
/**
* List all the known authors of stories.
*
return list;
}
+ /**
+ * Return the list of authors, grouped by starting letter(s) if needed.
+ * <p>
+ * If the number of author is not too high, only one group with an empty
+ * name and all the authors will be returned.
+ * <p>
+ * If not, the authors will be separated into groups:
+ * <ul>
+ * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
+ * </li>
+ * <li><tt>0-9</tt>: any authors whose name starts with a number</li>
+ * <li><tt>A-C</tt> (for instance): any author whose name starts with
+ * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li>
+ * </ul>
+ * Note that the letters used in the groups can vary (except <tt>*</tt> and
+ * <tt>0-9</tt>, which may only be present or not).
+ *
+ * @return the authors' names, grouped by letter(s)
+ */
+ public Map<String, List<String>> getAuthorsGrouped() {
+ int MAX = 20;
+
+ Map<String, List<String>> groups = new TreeMap<String, List<String>>();
+ List<String> authors = getAuthors();
+
+ // If all authors fit the max, just report them as is
+ if (authors.size() <= MAX) {
+ groups.put("", authors);
+ return groups;
+ }
+
+ // Create groups A to Z, which can be empty here
+ for (char car = 'A'; car <= 'Z'; car++) {
+ groups.put(Character.toString(car), getAuthorsGroup(authors, car));
+ }
+
+ // Collapse them
+ List<String> keys = new ArrayList<String>(groups.keySet());
+ for (int i = 0; i + 1 < keys.size(); i++) {
+ String keyNow = keys.get(i);
+ String keyNext = keys.get(i + 1);
+
+ List<String> now = groups.get(keyNow);
+ List<String> next = groups.get(keyNext);
+
+ int currentTotal = now.size() + next.size();
+ if (currentTotal <= MAX) {
+ String key = keyNow.charAt(0) + "-"
+ + keyNext.charAt(keyNext.length() - 1);
+
+ List<String> all = new ArrayList<String>();
+ all.addAll(now);
+ all.addAll(next);
+
+ groups.remove(keyNow);
+ groups.remove(keyNext);
+ groups.put(key, all);
+
+ keys.set(i, key); // set the new key instead of key(i)
+ keys.remove(i + 1); // remove the next, consumed key
+ i--; // restart at key(i)
+ }
+ }
+
+ // Add "special" groups
+ groups.put("*", getAuthorsGroup(authors, '*'));
+ groups.put("0-9", getAuthorsGroup(authors, '0'));
+
+ // Prune empty groups
+ keys = new ArrayList<String>(groups.keySet());
+ for (String key : keys) {
+ if (groups.get(key).isEmpty()) {
+ groups.remove(key);
+ }
+ }
+
+ return groups;
+ }
+
+ /**
+ * Get all the authors that start with the given character:
+ * <ul>
+ * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
+ * </li>
+ * <li><tt>0</tt>: any authors whose name starts with a number</li>
+ * <li><tt>A</tt> (any capital latin letter): any author whose name starts
+ * with <tt>A</tt></li>
+ * </ul>
+ *
+ * @param authors
+ * the full list of authors
+ * @param car
+ * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
+ * letter
+ * @return the authors that fulfill the starting letter
+ */
+ private List<String> getAuthorsGroup(List<String> authors, char car) {
+ List<String> accepted = new ArrayList<String>();
+ for (String author : authors) {
+ char first = '*';
+ for (int i = 0; first == '*' && i < author.length(); i++) {
+ String san = StringUtils.sanitize(author, true, true);
+ char c = san.charAt(i);
+ if (c >= '0' && c <= '9') {
+ first = '0';
+ } else if (c >= 'a' && c <= 'z') {
+ first = (char) (c - 'a' + 'A');
+ } else if (c >= 'A' && c <= 'Z') {
+ first = c;
+ }
+ }
+
+ if (first == car) {
+ accepted.add(author);
+ }
+ }
+
+ return accepted;
+ }
+
/**
* List all the stories in the {@link BasicLibrary}.
* <p>
- * Cover images not included.
+ * Cover images <b>MAYBE</b> not included.
*
* @return the stories
*/
.getType());
URL url = file.toURI().toURL();
if (type != null) {
-System.out.println("selected support: "+BasicSupport.getSupport(type, url));
story = BasicSupport.getSupport(type, url) //
.process(pgProcess);
+
// Because we do not want to clear the meta cache:
meta.setCover(story.getMeta().getCover());
+ meta.setResume(story.getMeta().getResume());
story.setMeta(meta);
//
} else {
* in case of I/O error
*/
public Story imprt(URL url, Progress pg) throws IOException {
+ if (pg == null)
+ pg = new Progress();
+
+ pg.setMinMax(0, 1000);
+ Progress pgProcess = new Progress();
+ Progress pgSave = new Progress();
+ pg.addProgress(pgProcess, 800);
+ pg.addProgress(pgSave, 200);
+
BasicSupport support = BasicSupport.getSupport(url);
if (support == null) {
throw new UnknownHostException("" + url);
}
- return save(support.process(pg), null);
+ Story story = save(support.process(pgProcess), pgSave);
+ pg.done();
+
+ return story;
}
/**
public synchronized Story save(Story story, String luid, Progress pg)
throws IOException {
+ Instance.getTraceHandler().trace(
+ this.getClass().getSimpleName() + ": saving story " + luid);
+
// Do not change the original metadata, but change the original story
MetaData meta = story.getMeta().clone();
story.setMeta(meta);
updateInfo(story.getMeta());
+ Instance.getTraceHandler().trace(
+ this.getClass().getSimpleName() + ": story saved (" + luid
+ + ")");
+
return story;
}
* in case of I/O error
*/
public synchronized void delete(String luid) throws IOException {
+ Instance.getTraceHandler().trace(
+ this.getClass().getSimpleName() + ": deleting story " + luid);
+
doDelete(luid);
- deleteInfo(luid);
+ invalidateInfo(luid);
+
+ Instance.getTraceHandler().trace(
+ this.getClass().getSimpleName() + ": story deleted (" + luid
+ + ")");
}
/**
throw new IOException("Story not found: " + luid);
}
+ changeSTA(luid, newSource, meta.getTitle(), meta.getAuthor(), pg);
+ }
+
+ /**
+ * Change the title (name) of the given {@link Story}.
+ *
+ * @param luid
+ * the {@link Story} LUID
+ * @param newTitle
+ * the new title
+ * @param pg
+ * the optional progress reporter
+ *
+ * @throws IOException
+ * in case of I/O error or if the {@link Story} was not found
+ */
+ public synchronized void changeTitle(String luid, String newTitle,
+ Progress pg) throws IOException {
+ MetaData meta = getInfo(luid);
+ if (meta == null) {
+ throw new IOException("Story not found: " + luid);
+ }
+
+ changeSTA(luid, meta.getSource(), newTitle, meta.getAuthor(), pg);
+ }
+
+ /**
+ * Change the author of the given {@link Story}.
+ *
+ * @param luid
+ * the {@link Story} LUID
+ * @param newAuthor
+ * the new author
+ * @param pg
+ * the optional progress reporter
+ *
+ * @throws IOException
+ * in case of I/O error or if the {@link Story} was not found
+ */
+ public synchronized void changeAuthor(String luid, String newAuthor,
+ Progress pg) throws IOException {
+ MetaData meta = getInfo(luid);
+ if (meta == null) {
+ throw new IOException("Story not found: " + luid);
+ }
+
+ changeSTA(luid, meta.getSource(), meta.getTitle(), newAuthor, pg);
+ }
+
+ /**
+ * Change the Source, Title and Author of the {@link Story} in one single
+ * go.
+ *
+ * @param luid
+ * the {@link Story} LUID
+ * @param newSource
+ * the new source
+ * @param newTitle
+ * the new title
+ * @param newAuthor
+ * the new author
+ * @param pg
+ * the optional progress reporter
+ *
+ * @throws IOException
+ * in case of I/O error or if the {@link Story} was not found
+ */
+ protected synchronized void changeSTA(String luid, String newSource,
+ String newTitle, String newAuthor, Progress pg) throws IOException {
+ MetaData meta = getInfo(luid);
+ if (meta == null) {
+ throw new IOException("Story not found: " + luid);
+ }
+
meta.setSource(newSource);
+ meta.setTitle(newTitle);
+ meta.setAuthor(newAuthor);
saveMeta(meta, pg);
}
* By default, delete the old {@link Story} then recreate a new
* {@link Story}.
* <p>
- * Note that this behaviour can lead to data loss.
+ * Note that this behaviour can lead to data loss in case of problems!
*
* @param meta
* the new {@link MetaData} (LUID <b>MUST NOT</b> change)
throw new IOException("Story not found: " + meta.getLuid());
}
+ // TODO: this is not safe!
delete(meta.getLuid());
-
story.setMeta(meta);
save(story, meta.getLuid(), pgSet);