Fix some CBZ cover/fake cover issues
[fanfix.git] / src / be / nikiroo / fanfix / library / BasicLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.awt.image.BufferedImage;
4 import java.io.File;
5 import java.io.IOException;
6 import java.net.URL;
7 import java.net.UnknownHostException;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.List;
11
12 import be.nikiroo.fanfix.Instance;
13 import be.nikiroo.fanfix.data.MetaData;
14 import be.nikiroo.fanfix.data.Story;
15 import be.nikiroo.fanfix.output.BasicOutput;
16 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
17 import be.nikiroo.fanfix.supported.BasicSupport;
18 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
19 import be.nikiroo.utils.Progress;
20
21 /**
22 * Manage a library of Stories: import, export, list, modify.
23 * <p>
24 * Each {@link Story} object will be associated with a (local to the library)
25 * unique ID, the LUID, which will be used to identify the {@link Story}.
26 * <p>
27 * Most of the {@link BasicLibrary} functions work on a partial (cover
28 * <b>MAY</b> not be included) {@link MetaData} object.
29 *
30 * @author niki
31 */
32 abstract public class BasicLibrary {
33 /**
34 * A {@link BasicLibrary} status.
35 *
36 * @author niki
37 */
38 public enum Status {
39 /** The library is ready. */
40 READY,
41 /** The library is invalid (not correctly set up). */
42 INVALID,
43 /** You are not allowed to access this library. */
44 UNAUTORIZED,
45 /** The library is currently out of commission. */
46 UNAVAILABLE,
47 }
48
49 /**
50 * Return a name for this library (the UI may display this).
51 * <p>
52 * Must not be NULL.
53 *
54 * @return the name, or an empty {@link String} if none
55 */
56 public String getLibraryName() {
57 return "";
58 }
59
60 /**
61 * The library status.
62 *
63 * @return the current status
64 */
65 public Status getStatus() {
66 return Status.READY;
67 }
68
69 /**
70 * Retrieve the main {@link File} corresponding to the given {@link Story},
71 * which can be passed to an external reader or instance.
72 * <p>
73 * Do <b>NOT</b> alter this file.
74 *
75 * @param luid
76 * the Library UID of the story
77 * @param pg
78 * the optional {@link Progress}
79 *
80 * @return the corresponding {@link Story}
81 */
82 public abstract File getFile(String luid, Progress pg);
83
84 /**
85 * Return the cover image associated to this story.
86 *
87 * @param luid
88 * the Library UID of the story
89 *
90 * @return the cover image
91 */
92 public abstract BufferedImage getCover(String luid);
93
94 /**
95 * Return the cover image associated to this source.
96 * <p>
97 * By default, return the cover of the first story with this source.
98 *
99 * @param source
100 * the source
101 *
102 * @return the cover image or NULL
103 */
104 public BufferedImage getSourceCover(String source) {
105 List<MetaData> metas = getListBySource(source);
106 if (metas.size() > 0) {
107 return getCover(metas.get(0).getLuid());
108 }
109
110 return null;
111 }
112
113 /**
114 * Fix the source cover to the given story cover.
115 *
116 * @param source
117 * the source to change
118 * @param luid
119 * the story LUID
120 */
121 public abstract void setSourceCover(String source, String luid);
122
123 /**
124 * Return the list of stories (represented by their {@link MetaData}, which
125 * <b>MAY</b> not have the cover included).
126 *
127 * @param pg
128 * the optional {@link Progress}
129 *
130 * @return the list (can be empty but not NULL)
131 */
132 protected abstract List<MetaData> getMetas(Progress pg);
133
134 /**
135 * Invalidate the {@link Story} cache (when the content should be re-read
136 * because it was changed).
137 */
138 protected abstract void clearCache();
139
140 /**
141 * Return the next LUID that can be used.
142 *
143 * @return the next luid
144 */
145 protected abstract int getNextId();
146
147 /**
148 * Delete the target {@link Story}.
149 *
150 * @param luid
151 * the LUID of the {@link Story}
152 *
153 * @throws IOException
154 * in case of I/O error or if the {@link Story} wa not found
155 */
156 protected abstract void doDelete(String luid) throws IOException;
157
158 /**
159 * Actually save the story to the back-end.
160 *
161 * @param story
162 * the {@link Story} to save
163 * @param pg
164 * the optional {@link Progress}
165 *
166 * @return the saved {@link Story} (which may have changed, especially
167 * regarding the {@link MetaData})
168 *
169 * @throws IOException
170 * in case of I/O error
171 */
172 protected abstract Story doSave(Story story, Progress pg)
173 throws IOException;
174
175 /**
176 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
177 * loaded.
178 *
179 * @param pg
180 * the optional progress reporter
181 */
182 public void refresh(Progress pg) {
183 getMetas(pg);
184 }
185
186 /**
187 * List all the known types (sources) of stories.
188 *
189 * @return the sources
190 */
191 public synchronized List<String> getSources() {
192 List<String> list = new ArrayList<String>();
193 for (MetaData meta : getMetas(null)) {
194 String storySource = meta.getSource();
195 if (!list.contains(storySource)) {
196 list.add(storySource);
197 }
198 }
199
200 Collections.sort(list);
201 return list;
202 }
203
204 /**
205 * List all the known authors of stories.
206 *
207 * @return the authors
208 */
209 public synchronized List<String> getAuthors() {
210 List<String> list = new ArrayList<String>();
211 for (MetaData meta : getMetas(null)) {
212 String storyAuthor = meta.getAuthor();
213 if (!list.contains(storyAuthor)) {
214 list.add(storyAuthor);
215 }
216 }
217
218 Collections.sort(list);
219 return list;
220 }
221
222 /**
223 * List all the stories in the {@link BasicLibrary}.
224 * <p>
225 * Cover images not included.
226 *
227 * @return the stories
228 */
229 public synchronized List<MetaData> getList() {
230 return getMetas(null);
231 }
232
233 /**
234 * List all the stories of the given source type in the {@link BasicLibrary}
235 * , or all the stories if NULL is passed as a type.
236 * <p>
237 * Cover images not included.
238 *
239 * @param type
240 * the type of story to retrieve, or NULL for all
241 *
242 * @return the stories
243 */
244 public synchronized List<MetaData> getListBySource(String type) {
245 List<MetaData> list = new ArrayList<MetaData>();
246 for (MetaData meta : getMetas(null)) {
247 String storyType = meta.getSource();
248 if (type == null || type.equalsIgnoreCase(storyType)) {
249 list.add(meta);
250 }
251 }
252
253 Collections.sort(list);
254 return list;
255 }
256
257 /**
258 * List all the stories of the given author in the {@link BasicLibrary}, or
259 * all the stories if NULL is passed as an author.
260 * <p>
261 * Cover images not included.
262 *
263 * @param author
264 * the author of the stories to retrieve, or NULL for all
265 *
266 * @return the stories
267 */
268 public synchronized List<MetaData> getListByAuthor(String author) {
269 List<MetaData> list = new ArrayList<MetaData>();
270 for (MetaData meta : getMetas(null)) {
271 String storyAuthor = meta.getAuthor();
272 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
273 list.add(meta);
274 }
275 }
276
277 Collections.sort(list);
278 return list;
279 }
280
281 /**
282 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
283 * cover image <b>MAY</b> not be included.
284 *
285 * @param luid
286 * the Library UID of the story
287 *
288 * @return the corresponding {@link Story}
289 */
290 public synchronized MetaData getInfo(String luid) {
291 if (luid != null) {
292 for (MetaData meta : getMetas(null)) {
293 if (luid.equals(meta.getLuid())) {
294 return meta;
295 }
296 }
297 }
298
299 return null;
300 }
301
302 /**
303 * Retrieve a specific {@link Story}.
304 *
305 * @param luid
306 * the Library UID of the story
307 * @param pg
308 * the optional progress reporter
309 *
310 * @return the corresponding {@link Story} or NULL if not found
311 */
312 public synchronized Story getStory(String luid, Progress pg) {
313 if (pg == null) {
314 pg = new Progress();
315 }
316
317 Progress pgGet = new Progress();
318 Progress pgProcess = new Progress();
319
320 pg.setMinMax(0, 2);
321 pg.addProgress(pgGet, 1);
322 pg.addProgress(pgProcess, 1);
323
324 Story story = null;
325 for (MetaData meta : getMetas(null)) {
326 if (meta.getLuid().equals(luid)) {
327 File file = getFile(luid, pgGet);
328 pgGet.done();
329 try {
330 SupportType type = SupportType.valueOfAllOkUC(meta
331 .getType());
332 URL url = file.toURI().toURL();
333 if (type != null) {
334 story = BasicSupport.getSupport(type).process(url,
335 pgProcess);
336 // Because we do not want to clear the meta cache:
337 meta.setCover(story.getMeta().getCover());
338 story.setMeta(meta);
339 //
340 } else {
341 throw new IOException("Unknown type: " + meta.getType());
342 }
343 } catch (IOException e) {
344 // We should not have not-supported files in the
345 // library
346 Instance.getTraceHandler().error(
347 new IOException("Cannot load file from library: "
348 + file, e));
349 } finally {
350 pgProcess.done();
351 pg.done();
352 }
353
354 break;
355 }
356 }
357
358 return story;
359 }
360
361 /**
362 * Import the {@link Story} at the given {@link URL} into the
363 * {@link BasicLibrary}.
364 *
365 * @param url
366 * the {@link URL} to import
367 * @param pg
368 * the optional progress reporter
369 *
370 * @return the imported {@link Story}
371 *
372 * @throws UnknownHostException
373 * if the host is not supported
374 * @throws IOException
375 * in case of I/O error
376 */
377 public Story imprt(URL url, Progress pg) throws IOException {
378 BasicSupport support = BasicSupport.getSupport(url);
379 if (support == null) {
380 throw new UnknownHostException("" + url);
381 }
382
383 return save(support.process(url, pg), null);
384 }
385
386 /**
387 * Import the story from one library to another, and keep the same LUID.
388 *
389 * @param other
390 * the other library to import from
391 * @param luid
392 * the Library UID
393 * @param pg
394 * the optional progress reporter
395 *
396 * @throws IOException
397 * in case of I/O error
398 */
399 public void imprt(BasicLibrary other, String luid, Progress pg)
400 throws IOException {
401 Progress pgGetStory = new Progress();
402 Progress pgSave = new Progress();
403 if (pg == null) {
404 pg = new Progress();
405 }
406
407 pg.setMinMax(0, 2);
408 pg.addProgress(pgGetStory, 1);
409 pg.addProgress(pgSave, 1);
410
411 Story story = other.getStory(luid, pgGetStory);
412 if (story != null) {
413 story = this.save(story, luid, pgSave);
414 pg.done();
415 } else {
416 pg.done();
417 throw new IOException("Cannot find story in Library: " + luid);
418 }
419 }
420
421 /**
422 * Export the {@link Story} to the given target in the given format.
423 *
424 * @param luid
425 * the {@link Story} ID
426 * @param type
427 * the {@link OutputType} to transform it to
428 * @param target
429 * the target to save to
430 * @param pg
431 * the optional progress reporter
432 *
433 * @return the saved resource (the main saved {@link File})
434 *
435 * @throws IOException
436 * in case of I/O error
437 */
438 public File export(String luid, OutputType type, String target, Progress pg)
439 throws IOException {
440 Progress pgGetStory = new Progress();
441 Progress pgOut = new Progress();
442 if (pg != null) {
443 pg.setMax(2);
444 pg.addProgress(pgGetStory, 1);
445 pg.addProgress(pgOut, 1);
446 }
447
448 BasicOutput out = BasicOutput.getOutput(type, false, false);
449 if (out == null) {
450 throw new IOException("Output type not supported: " + type);
451 }
452
453 Story story = getStory(luid, pgGetStory);
454 if (story == null) {
455 throw new IOException("Cannot find story to export: " + luid);
456 }
457
458 return out.process(story, target, pgOut);
459 }
460
461 /**
462 * Save a {@link Story} to the {@link BasicLibrary}.
463 *
464 * @param story
465 * the {@link Story} to save
466 * @param pg
467 * the optional progress reporter
468 *
469 * @return the same {@link Story}, whose LUID may have changed
470 *
471 * @throws IOException
472 * in case of I/O error
473 */
474 public Story save(Story story, Progress pg) throws IOException {
475 return save(story, null, pg);
476 }
477
478 /**
479 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
480 * be correct, or NULL to get the next free one.
481 * <p>
482 * Will override any previous {@link Story} with the same LUID.
483 *
484 * @param story
485 * the {@link Story} to save
486 * @param luid
487 * the <b>correct</b> LUID or NULL to get the next free one
488 * @param pg
489 * the optional progress reporter
490 *
491 * @return the same {@link Story}, whose LUID may have changed
492 *
493 * @throws IOException
494 * in case of I/O error
495 */
496 public synchronized Story save(Story story, String luid, Progress pg)
497 throws IOException {
498 // Do not change the original metadata, but change the original story
499 MetaData meta = story.getMeta().clone();
500 story.setMeta(meta);
501
502 if (luid == null || luid.isEmpty()) {
503 meta.setLuid(String.format("%03d", getNextId()));
504 } else {
505 meta.setLuid(luid);
506 }
507
508 if (getInfo(luid) != null) {
509 delete(luid);
510 }
511
512 doSave(story, pg);
513
514 clearCache();
515
516 return story;
517 }
518
519 /**
520 * Delete the given {@link Story} from this {@link BasicLibrary}.
521 *
522 * @param luid
523 * the LUID of the target {@link Story}
524 *
525 * @throws IOException
526 * in case of I/O error
527 */
528 public synchronized void delete(String luid) throws IOException {
529 doDelete(luid);
530 clearCache();
531 }
532
533 /**
534 * Change the type (source) of the given {@link Story}.
535 *
536 * @param luid
537 * the {@link Story} LUID
538 * @param newSource
539 * the new source
540 * @param pg
541 * the optional progress reporter
542 *
543 * @throws IOException
544 * in case of I/O error or if the {@link Story} was not found
545 */
546 public synchronized void changeSource(String luid, String newSource,
547 Progress pg) throws IOException {
548 MetaData meta = getInfo(luid);
549 if (meta == null) {
550 throw new IOException("Story not found: " + luid);
551 }
552
553 meta.setSource(newSource);
554 saveMeta(meta, pg);
555 }
556
557 /**
558 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
559 * change) for this {@link Story}.
560 * <p>
561 * By default, delete the old {@link Story} then recreate a new
562 * {@link Story}.
563 * <p>
564 * Note that this behaviour can lead to data loss.
565 *
566 * @param meta
567 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
568 * @param pg
569 * the optional {@link Progress}
570 *
571 * @throws IOException
572 * in case of I/O error or if the {@link Story} was not found
573 */
574 protected synchronized void saveMeta(MetaData meta, Progress pg)
575 throws IOException {
576 if (pg == null) {
577 pg = new Progress();
578 }
579
580 Progress pgGet = new Progress();
581 Progress pgSet = new Progress();
582 pg.addProgress(pgGet, 50);
583 pg.addProgress(pgSet, 50);
584
585 Story story = getStory(meta.getLuid(), pgGet);
586 if (story == null) {
587 throw new IOException("Story not found: " + meta.getLuid());
588 }
589
590 delete(meta.getLuid());
591
592 story.setMeta(meta);
593 save(story, meta.getLuid(), pgSet);
594
595 pg.done();
596 }
597 }