Perf improvement: covers
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix;
2
57f02339 3import java.awt.image.BufferedImage;
08fe2e33 4import java.io.File;
4f661b2b 5import java.io.FileFilter;
08fe2e33
NR
6import java.io.IOException;
7import java.net.URL;
8import java.util.ArrayList;
22848428 9import java.util.Collections;
08fe2e33
NR
10import java.util.HashMap;
11import java.util.List;
12import java.util.Map;
13import java.util.Map.Entry;
14
10d558d2 15import be.nikiroo.fanfix.bundles.Config;
08fe2e33
NR
16import be.nikiroo.fanfix.data.MetaData;
17import be.nikiroo.fanfix.data.Story;
18import be.nikiroo.fanfix.output.BasicOutput;
19import be.nikiroo.fanfix.output.BasicOutput.OutputType;
20import be.nikiroo.fanfix.supported.BasicSupport;
21import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
68686a37 22import be.nikiroo.fanfix.supported.InfoReader;
9843a5e5 23import be.nikiroo.utils.IOUtils;
3b2b638f 24import be.nikiroo.utils.Progress;
08fe2e33
NR
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}.
57f02339
NR
31 * <p>
32 * Most of the {@link Library} functions work on either the LUID or a partial
33 * (cover not included) {@link MetaData} object.
08fe2e33
NR
34 *
35 * @author niki
36 */
37public class Library {
38 private File baseDir;
39 private Map<MetaData, File> stories;
08fe2e33 40 private int lastId;
2206ef66
NR
41 private OutputType text;
42 private OutputType image;
08fe2e33
NR
43
44 /**
45 * Create a new {@link Library} with the given backend directory.
46 *
47 * @param dir
2206ef66
NR
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
08fe2e33 53 */
2206ef66 54 public Library(File dir, OutputType text, OutputType image) {
08fe2e33
NR
55 this.baseDir = dir;
56 this.stories = new HashMap<MetaData, File>();
57 this.lastId = 0;
2206ef66
NR
58 this.text = text;
59 this.image = image;
08fe2e33
NR
60
61 dir.mkdirs();
62 }
63
4f661b2b
NR
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
333f0e7b
NR
74 /**
75 * List all the known types of stories.
76 *
77 * @return the types
78 */
10d558d2 79 public synchronized List<String> getTypes() {
333f0e7b 80 List<String> list = new ArrayList<String>();
4f661b2b 81 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9 82 String storyType = entry.getKey().getSource();
333f0e7b
NR
83 if (!list.contains(storyType)) {
84 list.add(storyType);
85 }
86 }
87
4310bae9
NR
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>();
4f661b2b 99 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9
NR
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.
57f02339
NR
113 * <p>
114 * Cover images not included.
4310bae9
NR
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>();
4f661b2b 123 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9
NR
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);
333f0e7b
NR
131 return list;
132 }
133
08fe2e33
NR
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.
57f02339
NR
137 * <p>
138 * Cover images not included.
08fe2e33
NR
139 *
140 * @param type
141 * the type of story to retrieve, or NULL for all
142 *
143 * @return the stories
144 */
4310bae9 145 public synchronized List<MetaData> getListByType(String type) {
08fe2e33 146 List<MetaData> list = new ArrayList<MetaData>();
4f661b2b 147 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
08fe2e33 148 String storyType = entry.getValue().getParentFile().getName();
333f0e7b 149 if (type == null || type.equalsIgnoreCase(storyType)) {
08fe2e33
NR
150 list.add(entry.getKey());
151 }
152 }
153
22848428 154 Collections.sort(list);
08fe2e33
NR
155 return list;
156 }
157
301791d3 158 /**
57f02339
NR
159 * Retrieve a {@link File} corresponding to the given {@link Story}, cover
160 * image not included.
301791d3
NR
161 *
162 * @param luid
163 * the Library UID of the story
164 *
165 * @return the corresponding {@link Story}
166 */
10d558d2 167 public synchronized MetaData getInfo(String luid) {
301791d3 168 if (luid != null) {
4f661b2b 169 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
301791d3
NR
170 if (luid.equals(entry.getKey().getLuid())) {
171 return entry.getKey();
172 }
173 }
174 }
175
176 return null;
177 }
178
2206ef66
NR
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 */
10d558d2 187 public synchronized File getFile(String luid) {
2206ef66 188 if (luid != null) {
4f661b2b 189 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
2206ef66
NR
190 if (luid.equals(entry.getKey().getLuid())) {
191 return entry.getValue();
192 }
193 }
194 }
195
196 return null;
197 }
198
57f02339
NR
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
08fe2e33
NR
222 /**
223 * Retrieve a specific {@link Story}.
224 *
225 * @param luid
226 * the Library UID of the story
92fb0719
NR
227 * @param pg
228 * the optional progress reporter
08fe2e33 229 *
3d247bc3 230 * @return the corresponding {@link Story} or NULL if not found
08fe2e33 231 */
10d558d2 232 public synchronized Story getStory(String luid, Progress pg) {
08fe2e33 233 if (luid != null) {
4f661b2b 234 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
08fe2e33
NR
235 if (luid.equals(entry.getKey().getLuid())) {
236 try {
fe999aa4
NR
237 SupportType type = SupportType.valueOfAllOkUC(entry
238 .getKey().getType());
239 URL url = entry.getValue().toURI().toURL();
240 if (type != null) {
92fb0719
NR
241 return BasicSupport.getSupport(type).process(url,
242 pg);
fe999aa4
NR
243 } else {
244 throw new IOException("Unknown type: "
245 + entry.getKey().getType());
246 }
08fe2e33
NR
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
92fb0719
NR
258 if (pg != null) {
259 pg.setMinMax(0, 1);
260 pg.setProgress(1);
261 }
262
08fe2e33
NR
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
92fb0719
NR
272 * @param pg
273 * the optional progress reporter
08fe2e33
NR
274 *
275 * @return the imported {@link Story}
276 *
277 * @throws IOException
278 * in case of I/O error
279 */
92fb0719 280 public Story imprt(URL url, Progress pg) throws IOException {
08fe2e33
NR
281 BasicSupport support = BasicSupport.getSupport(url);
282 if (support == null) {
283 throw new IOException("URL not supported: " + url.toString());
284 }
285
92fb0719 286 return save(support.process(url, pg), null);
08fe2e33
NR
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
92fb0719
NR
298 * @param pg
299 * the optional progress reporter
08fe2e33
NR
300 *
301 * @return the saved resource (the main saved {@link File})
302 *
303 * @throws IOException
304 * in case of I/O error
305 */
92fb0719 306 public File export(String luid, OutputType type, String target, Progress pg)
08fe2e33 307 throws IOException {
bee7dffe
NR
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
08fe2e33
NR
316 BasicOutput out = BasicOutput.getOutput(type, true);
317 if (out == null) {
318 throw new IOException("Output type not supported: " + type);
319 }
320
bee7dffe 321 Story story = getStory(luid, pgGetStory);
73ce17ef
NR
322 if (story == null) {
323 throw new IOException("Cannot find story to export: " + luid);
324 }
325
bee7dffe 326 return out.process(story, target, pgOut);
08fe2e33
NR
327 }
328
329 /**
2206ef66
NR
330 * Save a {@link Story} to the {@link Library}.
331 *
332 * @param story
333 * the {@link Story} to save
bee7dffe
NR
334 * @param pg
335 * the optional progress reporter
2206ef66
NR
336 *
337 * @return the same {@link Story}, whose LUID may have changed
338 *
339 * @throws IOException
340 * in case of I/O error
341 */
bee7dffe
NR
342 public Story save(Story story, Progress pg) throws IOException {
343 return save(story, null, pg);
2206ef66
NR
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.
08fe2e33
NR
349 *
350 * @param story
351 * the {@link Story} to save
2206ef66
NR
352 * @param luid
353 * the <b>correct</b> LUID or NULL to get the next free one
bee7dffe
NR
354 * @param pg
355 * the optional progress reporter
2206ef66
NR
356 *
357 * @return the same {@link Story}, whose LUID may have changed
08fe2e33
NR
358 *
359 * @throws IOException
360 * in case of I/O error
361 */
10d558d2
NR
362 public synchronized Story save(Story story, String luid, Progress pg)
363 throws IOException {
a7d266e6 364 // Do not change the original metadata, but change the original story
301791d3 365 MetaData key = story.getMeta().clone();
a7d266e6 366 story.setMeta(key);
08fe2e33 367
2206ef66 368 if (luid == null || luid.isEmpty()) {
4f661b2b 369 getStories(null); // refresh lastId if needed
2206ef66
NR
370 key.setLuid(String.format("%03d", (++lastId)));
371 } else {
372 key.setLuid(luid);
373 }
374
08fe2e33
NR
375 getDir(key).mkdirs();
376 if (!getDir(key).exists()) {
377 throw new IOException("Cannot create library dir");
378 }
379
380 OutputType out;
08fe2e33 381 if (key != null && key.isImageDocument()) {
2206ef66 382 out = image;
08fe2e33 383 } else {
2206ef66 384 out = text;
08fe2e33 385 }
2206ef66 386
08fe2e33 387 BasicOutput it = BasicOutput.getOutput(out, true);
10d558d2
NR
388 it.process(story, getFile(key).getPath(), pg);
389
390 // empty cache
391 stories.clear();
2206ef66
NR
392
393 return story;
08fe2e33
NR
394 }
395
10d558d2
NR
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);
4f661b2b 408 File file = getStories(null).get(meta);
10d558d2
NR
409
410 if (file != null) {
411 if (file.delete()) {
9843a5e5
NR
412 String readerExt = getOutputType(meta)
413 .getDefaultExtension(true);
414 String fileExt = getOutputType(meta).getDefaultExtension(false);
10d558d2
NR
415
416 String path = file.getAbsolutePath();
9843a5e5
NR
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
10d558d2
NR
425 File infoFile = new File(path + ".info");
426 if (!infoFile.exists()) {
427 infoFile = new File(path.substring(0, path.length()
9843a5e5 428 - fileExt.length())
10d558d2
NR
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()
9843a5e5 439 - fileExt.length()));
10d558d2
NR
440 }
441 coverFile.delete();
442
443 ok = true;
444 }
445
446 // clear cache
447 stories.clear();
448 }
449
450 return ok;
451 }
452
08fe2e33
NR
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) {
a4143cd7
NR
477 String title = key.getTitle();
478 if (title == null) {
479 title = "";
480 }
481 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
08fe2e33
NR
482 return new File(getDir(key), key.getLuid() + "_" + title);
483 }
484
485 /**
486 * Return all the known stories in this {@link Library} object.
487 *
4f661b2b
NR
488 * @param pg
489 * the optional progress reporter
490 *
08fe2e33
NR
491 * @return the stories
492 */
4f661b2b
NR
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
08fe2e33
NR
500 if (stories.isEmpty()) {
501 lastId = 0;
68686a37 502
4f661b2b
NR
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
4f661b2b
NR
512 for (File dir : dirs) {
513 File[] files = dir.listFiles(new FileFilter() {
514 public boolean accept(File file) {
515 return file != null
57f02339
NR
516 && file.getPath().toLowerCase()
517 .endsWith(".info");
4f661b2b
NR
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) {
57f02339 526 pgFiles.setName(file.getName());
4f661b2b 527 try {
57f02339 528 Entry<MetaData, File> entry = readMeta(file, false);
08fe2e33 529 try {
57f02339 530 int id = Integer.parseInt(entry.getKey().getLuid());
4f661b2b
NR
531 if (id > lastId) {
532 lastId = id;
08fe2e33 533 }
4f661b2b 534
57f02339 535 stories.put(entry.getKey(), entry.getValue());
4f661b2b
NR
536 } catch (Exception e) {
537 // not normal!!
57f02339 538 throw new IOException(
4f661b2b
NR
539 "Cannot understand the LUID of "
540 + file.getPath() + ": "
57f02339 541 + entry.getKey().getLuid(), e);
08fe2e33 542 }
4f661b2b
NR
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));
08fe2e33 549 }
57f02339 550 pgFiles.add(1);
08fe2e33 551 }
4f661b2b
NR
552
553 pgFiles.setName(null);
08fe2e33 554 }
4f661b2b
NR
555
556 pgDirs.setName("Loading directories");
08fe2e33
NR
557 }
558
559 return stories;
560 }
2206ef66 561
57f02339
NR
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
2206ef66
NR
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 }
08fe2e33 606}