Perf improvement: covers
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
1 package be.nikiroo.fanfix;
2
3 import java.awt.image.BufferedImage;
4 import java.io.File;
5 import java.io.FileFilter;
6 import java.io.IOException;
7 import java.net.URL;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Map.Entry;
14
15 import be.nikiroo.fanfix.bundles.Config;
16 import be.nikiroo.fanfix.data.MetaData;
17 import be.nikiroo.fanfix.data.Story;
18 import be.nikiroo.fanfix.output.BasicOutput;
19 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
20 import be.nikiroo.fanfix.supported.BasicSupport;
21 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
22 import be.nikiroo.fanfix.supported.InfoReader;
23 import be.nikiroo.utils.IOUtils;
24 import be.nikiroo.utils.Progress;
25
26 /**
27 * Manage a library of Stories: import, export, list.
28 * <p>
29 * Each {@link Story} object will be associated with a (local to the library)
30 * unique ID, the LUID, which will be used to identify the {@link Story}.
31 * <p>
32 * Most of the {@link Library} functions work on either the LUID or a partial
33 * (cover not included) {@link MetaData} object.
34 *
35 * @author niki
36 */
37 public class Library {
38 private File baseDir;
39 private Map<MetaData, File> stories;
40 private int lastId;
41 private OutputType text;
42 private OutputType image;
43
44 /**
45 * Create a new {@link Library} with the given backend directory.
46 *
47 * @param dir
48 * the directory where to find the {@link Story} objects
49 * @param text
50 * the {@link OutputType} to save the text-focused stories into
51 * @param image
52 * the {@link OutputType} to save the images-focused stories into
53 */
54 public Library(File dir, OutputType text, OutputType image) {
55 this.baseDir = dir;
56 this.stories = new HashMap<MetaData, File>();
57 this.lastId = 0;
58 this.text = text;
59 this.image = image;
60
61 dir.mkdirs();
62 }
63
64 /**
65 * Refresh the {@link Library}, that is, make sure all stories are loaded.
66 *
67 * @param pg
68 * the optional progress reporter
69 */
70 public void refresh(Progress pg) {
71 getStories(pg);
72 }
73
74 /**
75 * List all the known types of stories.
76 *
77 * @return the types
78 */
79 public synchronized List<String> getTypes() {
80 List<String> list = new ArrayList<String>();
81 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
82 String storyType = entry.getKey().getSource();
83 if (!list.contains(storyType)) {
84 list.add(storyType);
85 }
86 }
87
88 Collections.sort(list);
89 return list;
90 }
91
92 /**
93 * List all the known authors of stories.
94 *
95 * @return the authors
96 */
97 public synchronized List<String> getAuthors() {
98 List<String> list = new ArrayList<String>();
99 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
100 String storyAuthor = entry.getKey().getAuthor();
101 if (!list.contains(storyAuthor)) {
102 list.add(storyAuthor);
103 }
104 }
105
106 Collections.sort(list);
107 return list;
108 }
109
110 /**
111 * List all the stories of the given author in the {@link Library}, or all
112 * the stories if NULL is passed as an author.
113 * <p>
114 * Cover images not included.
115 *
116 * @param author
117 * the author of the stories to retrieve, or NULL for all
118 *
119 * @return the stories
120 */
121 public synchronized List<MetaData> getListByAuthor(String author) {
122 List<MetaData> list = new ArrayList<MetaData>();
123 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
124 String storyAuthor = entry.getKey().getAuthor();
125 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
126 list.add(entry.getKey());
127 }
128 }
129
130 Collections.sort(list);
131 return list;
132 }
133
134 /**
135 * List all the stories of the given source type in the {@link Library}, or
136 * all the stories if NULL is passed as a type.
137 * <p>
138 * Cover images not included.
139 *
140 * @param type
141 * the type of story to retrieve, or NULL for all
142 *
143 * @return the stories
144 */
145 public synchronized List<MetaData> getListByType(String type) {
146 List<MetaData> list = new ArrayList<MetaData>();
147 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
148 String storyType = entry.getValue().getParentFile().getName();
149 if (type == null || type.equalsIgnoreCase(storyType)) {
150 list.add(entry.getKey());
151 }
152 }
153
154 Collections.sort(list);
155 return list;
156 }
157
158 /**
159 * Retrieve a {@link File} corresponding to the given {@link Story}, cover
160 * image not included.
161 *
162 * @param luid
163 * the Library UID of the story
164 *
165 * @return the corresponding {@link Story}
166 */
167 public synchronized MetaData getInfo(String luid) {
168 if (luid != null) {
169 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
170 if (luid.equals(entry.getKey().getLuid())) {
171 return entry.getKey();
172 }
173 }
174 }
175
176 return null;
177 }
178
179 /**
180 * Retrieve a {@link File} corresponding to the given {@link Story}.
181 *
182 * @param luid
183 * the Library UID of the story
184 *
185 * @return the corresponding {@link Story}
186 */
187 public synchronized File getFile(String luid) {
188 if (luid != null) {
189 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
190 if (luid.equals(entry.getKey().getLuid())) {
191 return entry.getValue();
192 }
193 }
194 }
195
196 return null;
197 }
198
199 /**
200 * Return the cover image associated to this story.
201 *
202 * @param luid
203 * the Library UID of the story
204 *
205 * @return the cover image
206 */
207 public synchronized BufferedImage getCover(String luid) {
208 MetaData meta = getInfo(luid);
209 if (meta != null) {
210 try {
211 File infoFile = new File(getFile(meta).getPath() + ".info");
212 meta = readMeta(infoFile, true).getKey();
213 return meta.getCover();
214 } catch (IOException e) {
215 Instance.syserr(e);
216 }
217 }
218
219 return null;
220 }
221
222 /**
223 * Retrieve a specific {@link Story}.
224 *
225 * @param luid
226 * the Library UID of the story
227 * @param pg
228 * the optional progress reporter
229 *
230 * @return the corresponding {@link Story} or NULL if not found
231 */
232 public synchronized Story getStory(String luid, Progress pg) {
233 if (luid != null) {
234 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
235 if (luid.equals(entry.getKey().getLuid())) {
236 try {
237 SupportType type = SupportType.valueOfAllOkUC(entry
238 .getKey().getType());
239 URL url = entry.getValue().toURI().toURL();
240 if (type != null) {
241 return BasicSupport.getSupport(type).process(url,
242 pg);
243 } else {
244 throw new IOException("Unknown type: "
245 + entry.getKey().getType());
246 }
247 } catch (IOException e) {
248 // We should not have not-supported files in the
249 // library
250 Instance.syserr(new IOException(
251 "Cannot load file from library: "
252 + entry.getValue().getPath(), e));
253 }
254 }
255 }
256 }
257
258 if (pg != null) {
259 pg.setMinMax(0, 1);
260 pg.setProgress(1);
261 }
262
263 return null;
264 }
265
266 /**
267 * Import the {@link Story} at the given {@link URL} into the
268 * {@link Library}.
269 *
270 * @param url
271 * the {@link URL} to import
272 * @param pg
273 * the optional progress reporter
274 *
275 * @return the imported {@link Story}
276 *
277 * @throws IOException
278 * in case of I/O error
279 */
280 public Story imprt(URL url, Progress pg) throws IOException {
281 BasicSupport support = BasicSupport.getSupport(url);
282 if (support == null) {
283 throw new IOException("URL not supported: " + url.toString());
284 }
285
286 return save(support.process(url, pg), null);
287 }
288
289 /**
290 * Export the {@link Story} to the given target in the given format.
291 *
292 * @param luid
293 * the {@link Story} ID
294 * @param type
295 * the {@link OutputType} to transform it to
296 * @param target
297 * the target to save to
298 * @param pg
299 * the optional progress reporter
300 *
301 * @return the saved resource (the main saved {@link File})
302 *
303 * @throws IOException
304 * in case of I/O error
305 */
306 public File export(String luid, OutputType type, String target, Progress pg)
307 throws IOException {
308 Progress pgGetStory = new Progress();
309 Progress pgOut = new Progress();
310 if (pg != null) {
311 pg.setMax(2);
312 pg.addProgress(pgGetStory, 1);
313 pg.addProgress(pgOut, 1);
314 }
315
316 BasicOutput out = BasicOutput.getOutput(type, true);
317 if (out == null) {
318 throw new IOException("Output type not supported: " + type);
319 }
320
321 Story story = getStory(luid, pgGetStory);
322 if (story == null) {
323 throw new IOException("Cannot find story to export: " + luid);
324 }
325
326 return out.process(story, target, pgOut);
327 }
328
329 /**
330 * Save a {@link Story} to the {@link Library}.
331 *
332 * @param story
333 * the {@link Story} to save
334 * @param pg
335 * the optional progress reporter
336 *
337 * @return the same {@link Story}, whose LUID may have changed
338 *
339 * @throws IOException
340 * in case of I/O error
341 */
342 public Story save(Story story, Progress pg) throws IOException {
343 return save(story, null, pg);
344 }
345
346 /**
347 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
348 * correct, or NULL to get the next free one.
349 *
350 * @param story
351 * the {@link Story} to save
352 * @param luid
353 * the <b>correct</b> LUID or NULL to get the next free one
354 * @param pg
355 * the optional progress reporter
356 *
357 * @return the same {@link Story}, whose LUID may have changed
358 *
359 * @throws IOException
360 * in case of I/O error
361 */
362 public synchronized Story save(Story story, String luid, Progress pg)
363 throws IOException {
364 // Do not change the original metadata, but change the original story
365 MetaData key = story.getMeta().clone();
366 story.setMeta(key);
367
368 if (luid == null || luid.isEmpty()) {
369 getStories(null); // refresh lastId if needed
370 key.setLuid(String.format("%03d", (++lastId)));
371 } else {
372 key.setLuid(luid);
373 }
374
375 getDir(key).mkdirs();
376 if (!getDir(key).exists()) {
377 throw new IOException("Cannot create library dir");
378 }
379
380 OutputType out;
381 if (key != null && key.isImageDocument()) {
382 out = image;
383 } else {
384 out = text;
385 }
386
387 BasicOutput it = BasicOutput.getOutput(out, true);
388 it.process(story, getFile(key).getPath(), pg);
389
390 // empty cache
391 stories.clear();
392
393 return story;
394 }
395
396 /**
397 * Delete the given {@link Story} from this {@link Library}.
398 *
399 * @param luid
400 * the LUID of the target {@link Story}
401 *
402 * @return TRUE if it was deleted
403 */
404 public synchronized boolean delete(String luid) {
405 boolean ok = false;
406
407 MetaData meta = getInfo(luid);
408 File file = getStories(null).get(meta);
409
410 if (file != null) {
411 if (file.delete()) {
412 String readerExt = getOutputType(meta)
413 .getDefaultExtension(true);
414 String fileExt = getOutputType(meta).getDefaultExtension(false);
415
416 String path = file.getAbsolutePath();
417 if (readerExt != null && !readerExt.equals(fileExt)) {
418 path = path
419 .substring(0, path.length() - readerExt.length())
420 + fileExt;
421 file = new File(path);
422 IOUtils.deltree(file);
423 }
424
425 File infoFile = new File(path + ".info");
426 if (!infoFile.exists()) {
427 infoFile = new File(path.substring(0, path.length()
428 - fileExt.length())
429 + ".info");
430 }
431 infoFile.delete();
432
433 String coverExt = "."
434 + Instance.getConfig().getString(
435 Config.IMAGE_FORMAT_COVER);
436 File coverFile = new File(path + coverExt);
437 if (!coverFile.exists()) {
438 coverFile = new File(path.substring(0, path.length()
439 - fileExt.length()));
440 }
441 coverFile.delete();
442
443 ok = true;
444 }
445
446 // clear cache
447 stories.clear();
448 }
449
450 return ok;
451 }
452
453 /**
454 * The directory (full path) where the {@link Story} related to this
455 * {@link MetaData} should be located on disk.
456 *
457 * @param key
458 * the {@link Story} {@link MetaData}
459 *
460 * @return the target directory
461 */
462 private File getDir(MetaData key) {
463 String source = key.getSource().replaceAll("[^a-zA-Z0-9._+-]", "_");
464 return new File(baseDir, source);
465 }
466
467 /**
468 * The target (full path) where the {@link Story} related to this
469 * {@link MetaData} should be located on disk.
470 *
471 * @param key
472 * the {@link Story} {@link MetaData}
473 *
474 * @return the target
475 */
476 private File getFile(MetaData key) {
477 String title = key.getTitle();
478 if (title == null) {
479 title = "";
480 }
481 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
482 return new File(getDir(key), key.getLuid() + "_" + title);
483 }
484
485 /**
486 * Return all the known stories in this {@link Library} object.
487 *
488 * @param pg
489 * the optional progress reporter
490 *
491 * @return the stories
492 */
493 private synchronized Map<MetaData, File> getStories(Progress pg) {
494 if (pg == null) {
495 pg = new Progress();
496 } else {
497 pg.setMinMax(0, 100);
498 }
499
500 if (stories.isEmpty()) {
501 lastId = 0;
502
503 File[] dirs = baseDir.listFiles(new FileFilter() {
504 public boolean accept(File file) {
505 return file != null && file.isDirectory();
506 }
507 });
508
509 Progress pgDirs = new Progress(0, 100 * dirs.length);
510 pg.addProgress(pgDirs, 100);
511
512 for (File dir : dirs) {
513 File[] files = dir.listFiles(new FileFilter() {
514 public boolean accept(File file) {
515 return file != null
516 && file.getPath().toLowerCase()
517 .endsWith(".info");
518 }
519 });
520
521 Progress pgFiles = new Progress(0, files.length);
522 pgDirs.addProgress(pgFiles, 100);
523 pgDirs.setName("Loading from: " + dir.getName());
524
525 for (File file : files) {
526 pgFiles.setName(file.getName());
527 try {
528 Entry<MetaData, File> entry = readMeta(file, false);
529 try {
530 int id = Integer.parseInt(entry.getKey().getLuid());
531 if (id > lastId) {
532 lastId = id;
533 }
534
535 stories.put(entry.getKey(), entry.getValue());
536 } catch (Exception e) {
537 // not normal!!
538 throw new IOException(
539 "Cannot understand the LUID of "
540 + file.getPath() + ": "
541 + entry.getKey().getLuid(), e);
542 }
543 } catch (IOException e) {
544 // We should not have not-supported files in the
545 // library
546 Instance.syserr(new IOException(
547 "Cannot load file from library: "
548 + file.getPath(), e));
549 }
550 pgFiles.add(1);
551 }
552
553 pgFiles.setName(null);
554 }
555
556 pgDirs.setName("Loading directories");
557 }
558
559 return stories;
560 }
561
562 private Entry<MetaData, File> readMeta(File infoFile, boolean withCover)
563 throws IOException {
564
565 final MetaData meta = InfoReader.readMeta(infoFile, withCover);
566
567 // Replace .info with whatever is needed:
568 String path = infoFile.getPath();
569 path = path.substring(0, path.length() - ".info".length());
570
571 String newExt = getOutputType(meta).getDefaultExtension(true);
572
573 File targetFile = new File(path + newExt);
574
575 final File ffile = targetFile;
576 return new Entry<MetaData, File>() {
577 public File setValue(File value) {
578 return null;
579 }
580
581 public File getValue() {
582 return ffile;
583 }
584
585 public MetaData getKey() {
586 return meta;
587 }
588 };
589 }
590
591 /**
592 * Return the {@link OutputType} for this {@link Story}.
593 *
594 * @param meta
595 * the {@link Story} {@link MetaData}
596 *
597 * @return the type
598 */
599 private OutputType getOutputType(MetaData meta) {
600 if (meta != null && meta.isImageDocument()) {
601 return image;
602 } else {
603 return text;
604 }
605 }
606 }