Fix remote timeot (try1) + reverse e621 /post/
[fanfix.git] / src / be / nikiroo / fanfix / library / BasicLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.URL;
6 import java.net.UnknownHostException;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.List;
10
11 import be.nikiroo.fanfix.Instance;
12 import be.nikiroo.fanfix.data.MetaData;
13 import be.nikiroo.fanfix.data.Story;
14 import be.nikiroo.fanfix.output.BasicOutput;
15 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
16 import be.nikiroo.fanfix.supported.BasicSupport;
17 import be.nikiroo.fanfix.supported.SupportType;
18 import be.nikiroo.utils.Image;
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 Image 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 Image 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 void deleteInfo() {
139 deleteInfo(null);
140 }
141
142 /**
143 * Invalidate the {@link Story} cache (when the content is removed).
144 * <p>
145 * All the cache can be deleted if NULL is passed as meta.
146 *
147 * @param luid
148 * the LUID of the {@link Story} to clear from the cache, or NULL
149 * for all stories
150 */
151 protected abstract void deleteInfo(String luid);
152
153 /**
154 * Invalidate the {@link Story} cache (when the content has changed, but we
155 * already have it) with the new given meta.
156 *
157 * @param meta
158 * the {@link Story} to clear from the cache
159 */
160 protected abstract void updateInfo(MetaData meta);
161
162 /**
163 * Return the next LUID that can be used.
164 *
165 * @return the next luid
166 */
167 protected abstract int getNextId();
168
169 /**
170 * Delete the target {@link Story}.
171 *
172 * @param luid
173 * the LUID of the {@link Story}
174 *
175 * @throws IOException
176 * in case of I/O error or if the {@link Story} wa not found
177 */
178 protected abstract void doDelete(String luid) throws IOException;
179
180 /**
181 * Actually save the story to the back-end.
182 *
183 * @param story
184 * the {@link Story} to save
185 * @param pg
186 * the optional {@link Progress}
187 *
188 * @return the saved {@link Story} (which may have changed, especially
189 * regarding the {@link MetaData})
190 *
191 * @throws IOException
192 * in case of I/O error
193 */
194 protected abstract Story doSave(Story story, Progress pg)
195 throws IOException;
196
197 /**
198 * Refresh the {@link BasicLibrary}, that is, make sure all metas are
199 * loaded.
200 *
201 * @param pg
202 * the optional progress reporter
203 */
204 public void refresh(Progress pg) {
205 getMetas(pg);
206 }
207
208 /**
209 * List all the known types (sources) of stories.
210 *
211 * @return the sources
212 */
213 public synchronized List<String> getSources() {
214 List<String> list = new ArrayList<String>();
215 for (MetaData meta : getMetas(null)) {
216 String storySource = meta.getSource();
217 if (!list.contains(storySource)) {
218 list.add(storySource);
219 }
220 }
221
222 Collections.sort(list);
223 return list;
224 }
225
226 /**
227 * List all the known authors of stories.
228 *
229 * @return the authors
230 */
231 public synchronized List<String> getAuthors() {
232 List<String> list = new ArrayList<String>();
233 for (MetaData meta : getMetas(null)) {
234 String storyAuthor = meta.getAuthor();
235 if (!list.contains(storyAuthor)) {
236 list.add(storyAuthor);
237 }
238 }
239
240 Collections.sort(list);
241 return list;
242 }
243
244 /**
245 * List all the stories in the {@link BasicLibrary}.
246 * <p>
247 * Cover images not included.
248 *
249 * @return the stories
250 */
251 public synchronized List<MetaData> getList() {
252 return getMetas(null);
253 }
254
255 /**
256 * List all the stories of the given source type in the {@link BasicLibrary}
257 * , or all the stories if NULL is passed as a type.
258 * <p>
259 * Cover images not included.
260 *
261 * @param type
262 * the type of story to retrieve, or NULL for all
263 *
264 * @return the stories
265 */
266 public synchronized List<MetaData> getListBySource(String type) {
267 List<MetaData> list = new ArrayList<MetaData>();
268 for (MetaData meta : getMetas(null)) {
269 String storyType = meta.getSource();
270 if (type == null || type.equalsIgnoreCase(storyType)) {
271 list.add(meta);
272 }
273 }
274
275 Collections.sort(list);
276 return list;
277 }
278
279 /**
280 * List all the stories of the given author in the {@link BasicLibrary}, or
281 * all the stories if NULL is passed as an author.
282 * <p>
283 * Cover images not included.
284 *
285 * @param author
286 * the author of the stories to retrieve, or NULL for all
287 *
288 * @return the stories
289 */
290 public synchronized List<MetaData> getListByAuthor(String author) {
291 List<MetaData> list = new ArrayList<MetaData>();
292 for (MetaData meta : getMetas(null)) {
293 String storyAuthor = meta.getAuthor();
294 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
295 list.add(meta);
296 }
297 }
298
299 Collections.sort(list);
300 return list;
301 }
302
303 /**
304 * Retrieve a {@link MetaData} corresponding to the given {@link Story},
305 * cover image <b>MAY</b> not be included.
306 *
307 * @param luid
308 * the Library UID of the story
309 *
310 * @return the corresponding {@link Story}
311 */
312 public synchronized MetaData getInfo(String luid) {
313 if (luid != null) {
314 for (MetaData meta : getMetas(null)) {
315 if (luid.equals(meta.getLuid())) {
316 return meta;
317 }
318 }
319 }
320
321 return null;
322 }
323
324 /**
325 * Retrieve a specific {@link Story}.
326 *
327 * @param luid
328 * the Library UID of the story
329 * @param pg
330 * the optional progress reporter
331 *
332 * @return the corresponding {@link Story} or NULL if not found
333 */
334 public synchronized Story getStory(String luid, Progress pg) {
335 if (pg == null) {
336 pg = new Progress();
337 }
338
339 Progress pgGet = new Progress();
340 Progress pgProcess = new Progress();
341
342 pg.setMinMax(0, 2);
343 pg.addProgress(pgGet, 1);
344 pg.addProgress(pgProcess, 1);
345
346 Story story = null;
347 for (MetaData meta : getMetas(null)) {
348 if (meta.getLuid().equals(luid)) {
349 File file = getFile(luid, pgGet);
350 pgGet.done();
351 try {
352 SupportType type = SupportType.valueOfAllOkUC(meta
353 .getType());
354 URL url = file.toURI().toURL();
355 if (type != null) {
356 story = BasicSupport.getSupport(type, url) //
357 .process(pgProcess);
358
359 // Because we do not want to clear the meta cache:
360 meta.setCover(story.getMeta().getCover());
361 story.setMeta(meta);
362 //
363 } else {
364 throw new IOException("Unknown type: " + meta.getType());
365 }
366 } catch (IOException e) {
367 // We should not have not-supported files in the
368 // library
369 Instance.getTraceHandler().error(
370 new IOException("Cannot load file from library: "
371 + file, e));
372 } finally {
373 pgProcess.done();
374 pg.done();
375 }
376
377 break;
378 }
379 }
380
381 return story;
382 }
383
384 /**
385 * Import the {@link Story} at the given {@link URL} into the
386 * {@link BasicLibrary}.
387 *
388 * @param url
389 * the {@link URL} to import
390 * @param pg
391 * the optional progress reporter
392 *
393 * @return the imported {@link Story}
394 *
395 * @throws UnknownHostException
396 * if the host is not supported
397 * @throws IOException
398 * in case of I/O error
399 */
400 public Story imprt(URL url, Progress pg) throws IOException {
401 if (pg == null)
402 pg = new Progress();
403
404 pg.setMinMax(0, 1000);
405 Progress pgProcess = new Progress();
406 Progress pgSave = new Progress();
407 pg.addProgress(pgProcess, 800);
408 pg.addProgress(pgSave, 200);
409
410 BasicSupport support = BasicSupport.getSupport(url);
411 if (support == null) {
412 throw new UnknownHostException("" + url);
413 }
414
415 Story story = save(support.process(pgProcess), pgSave);
416 pg.done();
417
418 return story;
419 }
420
421 /**
422 * Import the story from one library to another, and keep the same LUID.
423 *
424 * @param other
425 * the other library to import from
426 * @param luid
427 * the Library UID
428 * @param pg
429 * the optional progress reporter
430 *
431 * @throws IOException
432 * in case of I/O error
433 */
434 public void imprt(BasicLibrary other, String luid, Progress pg)
435 throws IOException {
436 Progress pgGetStory = new Progress();
437 Progress pgSave = new Progress();
438 if (pg == null) {
439 pg = new Progress();
440 }
441
442 pg.setMinMax(0, 2);
443 pg.addProgress(pgGetStory, 1);
444 pg.addProgress(pgSave, 1);
445
446 Story story = other.getStory(luid, pgGetStory);
447 if (story != null) {
448 story = this.save(story, luid, pgSave);
449 pg.done();
450 } else {
451 pg.done();
452 throw new IOException("Cannot find story in Library: " + luid);
453 }
454 }
455
456 /**
457 * Export the {@link Story} to the given target in the given format.
458 *
459 * @param luid
460 * the {@link Story} ID
461 * @param type
462 * the {@link OutputType} to transform it to
463 * @param target
464 * the target to save to
465 * @param pg
466 * the optional progress reporter
467 *
468 * @return the saved resource (the main saved {@link File})
469 *
470 * @throws IOException
471 * in case of I/O error
472 */
473 public File export(String luid, OutputType type, String target, Progress pg)
474 throws IOException {
475 Progress pgGetStory = new Progress();
476 Progress pgOut = new Progress();
477 if (pg != null) {
478 pg.setMax(2);
479 pg.addProgress(pgGetStory, 1);
480 pg.addProgress(pgOut, 1);
481 }
482
483 BasicOutput out = BasicOutput.getOutput(type, false, false);
484 if (out == null) {
485 throw new IOException("Output type not supported: " + type);
486 }
487
488 Story story = getStory(luid, pgGetStory);
489 if (story == null) {
490 throw new IOException("Cannot find story to export: " + luid);
491 }
492
493 return out.process(story, target, pgOut);
494 }
495
496 /**
497 * Save a {@link Story} to the {@link BasicLibrary}.
498 *
499 * @param story
500 * the {@link Story} to save
501 * @param pg
502 * the optional progress reporter
503 *
504 * @return the same {@link Story}, whose LUID may have changed
505 *
506 * @throws IOException
507 * in case of I/O error
508 */
509 public Story save(Story story, Progress pg) throws IOException {
510 return save(story, null, pg);
511 }
512
513 /**
514 * Save a {@link Story} to the {@link BasicLibrary} -- the LUID <b>must</b>
515 * be correct, or NULL to get the next free one.
516 * <p>
517 * Will override any previous {@link Story} with the same LUID.
518 *
519 * @param story
520 * the {@link Story} to save
521 * @param luid
522 * the <b>correct</b> LUID or NULL to get the next free one
523 * @param pg
524 * the optional progress reporter
525 *
526 * @return the same {@link Story}, whose LUID may have changed
527 *
528 * @throws IOException
529 * in case of I/O error
530 */
531 public synchronized Story save(Story story, String luid, Progress pg)
532 throws IOException {
533
534 Instance.getTraceHandler().trace(
535 this.getClass().getSimpleName() + ": saving story " + luid);
536
537 // Do not change the original metadata, but change the original story
538 MetaData meta = story.getMeta().clone();
539 story.setMeta(meta);
540
541 if (luid == null || luid.isEmpty()) {
542 meta.setLuid(String.format("%03d", getNextId()));
543 } else {
544 meta.setLuid(luid);
545 }
546
547 if (luid != null && getInfo(luid) != null) {
548 delete(luid);
549 }
550
551 story = doSave(story, pg);
552
553 updateInfo(story.getMeta());
554
555 Instance.getTraceHandler().trace(
556 this.getClass().getSimpleName() + ": story saved (" + luid
557 + ")");
558
559 return story;
560 }
561
562 /**
563 * Delete the given {@link Story} from this {@link BasicLibrary}.
564 *
565 * @param luid
566 * the LUID of the target {@link Story}
567 *
568 * @throws IOException
569 * in case of I/O error
570 */
571 public synchronized void delete(String luid) throws IOException {
572 Instance.getTraceHandler().trace(
573 this.getClass().getSimpleName() + ": deleting story " + luid);
574
575 doDelete(luid);
576 deleteInfo(luid);
577
578 Instance.getTraceHandler().trace(
579 this.getClass().getSimpleName() + ": story deleted (" + luid
580 + ")");
581 }
582
583 /**
584 * Change the type (source) of the given {@link Story}.
585 *
586 * @param luid
587 * the {@link Story} LUID
588 * @param newSource
589 * the new source
590 * @param pg
591 * the optional progress reporter
592 *
593 * @throws IOException
594 * in case of I/O error or if the {@link Story} was not found
595 */
596 public synchronized void changeSource(String luid, String newSource,
597 Progress pg) throws IOException {
598 MetaData meta = getInfo(luid);
599 if (meta == null) {
600 throw new IOException("Story not found: " + luid);
601 }
602
603 meta.setSource(newSource);
604 saveMeta(meta, pg);
605 }
606
607 /**
608 * Save back the current state of the {@link MetaData} (LUID <b>MUST NOT</b>
609 * change) for this {@link Story}.
610 * <p>
611 * By default, delete the old {@link Story} then recreate a new
612 * {@link Story}.
613 * <p>
614 * Note that this behaviour can lead to data loss.
615 *
616 * @param meta
617 * the new {@link MetaData} (LUID <b>MUST NOT</b> change)
618 * @param pg
619 * the optional {@link Progress}
620 *
621 * @throws IOException
622 * in case of I/O error or if the {@link Story} was not found
623 */
624 protected synchronized void saveMeta(MetaData meta, Progress pg)
625 throws IOException {
626 if (pg == null) {
627 pg = new Progress();
628 }
629
630 Progress pgGet = new Progress();
631 Progress pgSet = new Progress();
632 pg.addProgress(pgGet, 50);
633 pg.addProgress(pgSet, 50);
634
635 Story story = getStory(meta.getLuid(), pgGet);
636 if (story == null) {
637 throw new IOException("Story not found: " + meta.getLuid());
638 }
639
640 delete(meta.getLuid());
641
642 story.setMeta(meta);
643 save(story, meta.getLuid(), pgSet);
644
645 pg.done();
646 }
647 }