fix cover issue
[fanfix.git] / src / be / nikiroo / fanfix / library / LocalLibrary.java
CommitLineData
e42573a0 1package be.nikiroo.fanfix.library;
68e2c6d2 2
68e2c6d2
NR
3import java.io.File;
4import java.io.FileFilter;
14b57448 5import java.io.FileInputStream;
e1de8087 6import java.io.FileNotFoundException;
68e2c6d2 7import java.io.IOException;
14b57448 8import java.io.InputStream;
68e2c6d2
NR
9import java.util.ArrayList;
10import java.util.HashMap;
11import java.util.List;
12import java.util.Map;
13
e42573a0 14import be.nikiroo.fanfix.Instance;
68e2c6d2
NR
15import be.nikiroo.fanfix.bundles.Config;
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.output.InfoCover;
21import be.nikiroo.fanfix.supported.InfoReader;
22import be.nikiroo.utils.IOUtils;
16a81ef7 23import be.nikiroo.utils.Image;
68e2c6d2
NR
24import be.nikiroo.utils.Progress;
25
26/**
27 * This {@link BasicLibrary} will store the stories locally on disk.
28 *
29 * @author niki
30 */
31public class LocalLibrary extends BasicLibrary {
32 private int lastId;
33 private Map<MetaData, File[]> stories; // Files: [ infoFile, TargetFile ]
16a81ef7 34 private Map<String, Image> sourceCovers;
68e2c6d2
NR
35
36 private File baseDir;
37 private OutputType text;
38 private OutputType image;
39
e604986c
NR
40 /**
41 * Create a new {@link LocalLibrary} with the given back-end directory.
42 *
43 * @param baseDir
44 * the directory where to find the {@link Story} objects
45 */
46 public LocalLibrary(File baseDir) {
47 this(baseDir, Instance.getConfig().getString(
48 Config.NON_IMAGES_DOCUMENT_TYPE), Instance.getConfig()
49 .getString(Config.IMAGES_DOCUMENT_TYPE), false);
50 }
51
52 /**
53 * Create a new {@link LocalLibrary} with the given back-end directory.
54 *
55 * @param baseDir
56 * the directory where to find the {@link Story} objects
ff05b828
NR
57 * @param text
58 * the {@link OutputType} to use for non-image documents
59 * @param image
60 * the {@link OutputType} to use for image documents
61 * @param defaultIsHtml
62 * if the given text or image is invalid, use HTML by default (if
63 * not, it will be INFO_TEXT/CBZ by default)
e604986c
NR
64 */
65 public LocalLibrary(File baseDir, String text, String image,
66 boolean defaultIsHtml) {
ff05b828 67 this(baseDir, OutputType.valueOfAllOkUC(text,
e604986c 68 defaultIsHtml ? OutputType.HTML : OutputType.INFO_TEXT),
ff05b828 69 OutputType.valueOfAllOkUC(image,
e604986c
NR
70 defaultIsHtml ? OutputType.HTML : OutputType.CBZ));
71 }
72
68e2c6d2
NR
73 /**
74 * Create a new {@link LocalLibrary} with the given back-end directory.
75 *
76 * @param baseDir
77 * the directory where to find the {@link Story} objects
78 * @param text
e604986c 79 * the {@link OutputType} to use for non-image documents
68e2c6d2 80 * @param image
e604986c 81 * the {@link OutputType} to use for image documents
68e2c6d2
NR
82 */
83 public LocalLibrary(File baseDir, OutputType text, OutputType image) {
84 this.baseDir = baseDir;
85 this.text = text;
86 this.image = image;
87
88 this.lastId = 0;
89 this.stories = null;
b56c9d60 90 this.sourceCovers = null;
68e2c6d2
NR
91
92 baseDir.mkdirs();
93 }
94
95 @Override
96 protected List<MetaData> getMetas(Progress pg) {
97 return new ArrayList<MetaData>(getStories(pg).keySet());
98 }
99
100 @Override
ff05b828 101 public File getFile(String luid, Progress pg) {
9e2fad36
NR
102 Instance.getTraceHandler().trace(
103 this.getClass().getSimpleName() + ": get file for " + luid);
104
105 File file = null;
106 String mess = "no file found for ";
107
108 MetaData meta = getInfo(luid);
9e2fad36 109 File[] files = getStories(pg).get(meta);
68e2c6d2 110 if (files != null) {
9e2fad36
NR
111 mess = "file retrieved for ";
112 file = files[1];
68e2c6d2
NR
113 }
114
9e2fad36 115 Instance.getTraceHandler().trace(
9b863b20
NR
116 this.getClass().getSimpleName() + ": " + mess + luid + " ("
117 + meta.getTitle() + ")");
9e2fad36
NR
118
119 return file;
68e2c6d2
NR
120 }
121
122 @Override
16a81ef7 123 public Image getCover(String luid) {
68e2c6d2
NR
124 MetaData meta = getInfo(luid);
125 if (meta != null) {
126 File[] files = getStories(null).get(meta);
127 if (files != null) {
128 File infoFile = files[0];
129
130 try {
131 meta = InfoReader.readMeta(infoFile, true);
132 return meta.getCover();
133 } catch (IOException e) {
62c63b07 134 Instance.getTraceHandler().error(e);
68e2c6d2
NR
135 }
136 }
137 }
138
139 return null;
140 }
141
142 @Override
efa3c511
NR
143 protected synchronized void updateInfo(MetaData meta) {
144 deleteInfo();
145 }
146
147 @Override
148 protected void deleteInfo(String luid) {
68e2c6d2 149 stories = null;
b56c9d60 150 sourceCovers = null;
68e2c6d2
NR
151 }
152
153 @Override
154 protected synchronized int getNextId() {
14b57448 155 getStories(null); // make sure lastId is set
68e2c6d2
NR
156 return ++lastId;
157 }
158
159 @Override
160 protected void doDelete(String luid) throws IOException {
161 for (File file : getRelatedFiles(luid)) {
162 // TODO: throw an IOException if we cannot delete the files?
163 IOUtils.deltree(file);
e272f05f 164 file.getParentFile().delete();
68e2c6d2
NR
165 }
166 }
167
168 @Override
169 protected Story doSave(Story story, Progress pg) throws IOException {
170 MetaData meta = story.getMeta();
171
172 File expectedTarget = getExpectedFile(meta);
173 expectedTarget.getParentFile().mkdirs();
174
925298fd 175 BasicOutput it = BasicOutput.getOutput(getOutputType(meta), true, true);
68e2c6d2
NR
176 it.process(story, expectedTarget.getPath(), pg);
177
178 return story;
179 }
180
181 @Override
182 protected synchronized void saveMeta(MetaData meta, Progress pg)
183 throws IOException {
184 File newDir = getExpectedDir(meta.getSource());
185 if (!newDir.exists()) {
186 newDir.mkdir();
187 }
188
189 List<File> relatedFiles = getRelatedFiles(meta.getLuid());
190 for (File relatedFile : relatedFiles) {
191 // TODO: this is not safe at all.
192 // We should copy all the files THEN delete them
193 // Maybe also adding some rollback cleanup if possible
194 if (relatedFile.getName().endsWith(".info")) {
195 try {
196 String name = relatedFile.getName().replaceFirst(
197 "\\.info$", "");
198 InfoCover.writeInfo(newDir, name, meta);
199 relatedFile.delete();
e272f05f 200 relatedFile.getParentFile().delete();
68e2c6d2 201 } catch (IOException e) {
62c63b07 202 Instance.getTraceHandler().error(e);
68e2c6d2
NR
203 }
204 } else {
205 relatedFile.renameTo(new File(newDir, relatedFile.getName()));
e272f05f 206 relatedFile.getParentFile().delete();
68e2c6d2
NR
207 }
208 }
209
efa3c511 210 deleteInfo();
68e2c6d2
NR
211 }
212
14b57448 213 @Override
e1de8087 214 public synchronized Image getCustomSourceCover(String source) {
b56c9d60 215 if (sourceCovers == null) {
e1de8087
NR
216 sourceCovers = new HashMap<String, Image>();
217 }
218
219 Image img = sourceCovers.get(source);
220 if (img != null) {
221 return img;
b56c9d60
NR
222 }
223
e1de8087
NR
224 File coverDir = new File(baseDir, source);
225 if (coverDir.isDirectory()) {
226 File cover = new File(coverDir, ".cover.png");
a09ef2bb
NR
227 if (cover.exists()) {
228 InputStream in;
e1de8087 229 try {
a09ef2bb
NR
230 in = new FileInputStream(cover);
231 try {
232 sourceCovers.put(source, new Image(in));
233 } finally {
234 in.close();
235 }
236 } catch (FileNotFoundException e) {
237 e.printStackTrace();
238 } catch (IOException e) {
239 Instance.getTraceHandler().error(
240 new IOException(
241 "Cannot load the existing custom source cover: "
242 + cover, e));
e1de8087 243 }
e1de8087 244 }
14b57448
NR
245 }
246
247 return sourceCovers.get(source);
248 }
249
250 @Override
251 public void setSourceCover(String source, String luid) {
e1de8087
NR
252 setSourceCover(source, getCover(luid));
253 }
b56c9d60 254
e1de8087
NR
255 /**
256 * Fix the source cover to the given story cover.
257 *
258 * @param source
259 * the source to change
260 * @param coverImage
261 * the cover image
262 */
263 synchronized void setSourceCover(String source, Image coverImage) {
ecfb936e 264 File cover = new File(getExpectedDir(source), ".cover");
14b57448 265 try {
e1de8087
NR
266 Instance.getCache().saveAsImage(coverImage, cover, true);
267 if (sourceCovers != null) {
268 sourceCovers.put(source, coverImage);
269 }
14b57448 270 } catch (IOException e) {
62c63b07 271 Instance.getTraceHandler().error(e);
14b57448
NR
272 }
273 }
274
b89dfb6e
NR
275 @Override
276 public void imprt(BasicLibrary other, String luid, Progress pg)
277 throws IOException {
278 if (pg == null) {
279 pg = new Progress();
280 }
281
ff05b828 282 // Check if we can simply copy the files instead of the whole process
b89dfb6e 283 if (other instanceof LocalLibrary) {
ff05b828 284 LocalLibrary otherLocalLibrary = (LocalLibrary) other;
e604986c 285
e604986c
NR
286 MetaData meta = otherLocalLibrary.getInfo(luid);
287 String expectedType = ""
288 + (meta != null && meta.isImageDocument() ? image : text);
289 if (meta != null && meta.getType().equals(expectedType)) {
290 File from = otherLocalLibrary.getExpectedDir(meta.getSource());
b89dfb6e 291 File to = this.getExpectedDir(meta.getSource());
e604986c 292 List<File> sources = otherLocalLibrary.getRelatedFiles(luid);
b89dfb6e
NR
293 if (!sources.isEmpty()) {
294 pg.setMinMax(0, sources.size());
295 }
296
297 for (File source : sources) {
298 File target = new File(source.getAbsolutePath().replace(
299 from.getAbsolutePath(), to.getAbsolutePath()));
300 if (!source.equals(target)) {
e604986c 301 target.getParentFile().mkdirs();
b89dfb6e
NR
302 InputStream in = null;
303 try {
304 in = new FileInputStream(source);
305 IOUtils.write(in, target);
306 } catch (IOException e) {
307 if (in != null) {
308 try {
309 in.close();
310 } catch (Exception ee) {
311 }
312 }
313
314 pg.done();
315 throw e;
316 }
317 }
318
319 pg.add(1);
320 }
321
efa3c511 322 deleteInfo();
b89dfb6e
NR
323 pg.done();
324 return;
325 }
326 }
327
328 super.imprt(other, luid, pg);
329 }
330
68e2c6d2
NR
331 /**
332 * Return the {@link OutputType} for this {@link Story}.
333 *
334 * @param meta
335 * the {@link Story} {@link MetaData}
336 *
337 * @return the type
338 */
339 private OutputType getOutputType(MetaData meta) {
340 if (meta != null && meta.isImageDocument()) {
341 return image;
68e2c6d2 342 }
211f7ddb
NR
343
344 return text;
68e2c6d2
NR
345 }
346
347 /**
348 * Get the target {@link File} related to the given <tt>.info</tt>
349 * {@link File} and {@link MetaData}.
350 *
351 * @param meta
352 * the meta
353 * @param infoFile
354 * the <tt>.info</tt> {@link File}
355 *
356 * @return the target {@link File}
357 */
358 private File getTargetFile(MetaData meta, File infoFile) {
359 // Replace .info with whatever is needed:
360 String path = infoFile.getPath();
361 path = path.substring(0, path.length() - ".info".length());
362 String newExt = getOutputType(meta).getDefaultExtension(true);
363
364 return new File(path + newExt);
365 }
366
367 /**
368 * The target (full path) where the {@link Story} related to this
369 * {@link MetaData} should be located on disk for a new {@link Story}.
370 *
371 * @param key
372 * the {@link Story} {@link MetaData}
373 *
374 * @return the target
375 */
376 private File getExpectedFile(MetaData key) {
377 String title = key.getTitle();
378 if (title == null) {
379 title = "";
380 }
381 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
382 return new File(getExpectedDir(key.getSource()), key.getLuid() + "_"
383 + title);
384 }
385
386 /**
387 * The directory (full path) where the new {@link Story} related to this
388 * {@link MetaData} should be located on disk.
389 *
085a2f9a 390 * @param source
68e2c6d2
NR
391 * the type (source)
392 *
393 * @return the target directory
394 */
085a2f9a
NR
395 private File getExpectedDir(String source) {
396 String sanitizedSource = source.replaceAll("[^a-zA-Z0-9._+-]", "_");
397 return new File(baseDir, sanitizedSource);
68e2c6d2
NR
398 }
399
400 /**
401 * Return the list of files/directories on disk for this {@link Story}.
402 * <p>
403 * If the {@link Story} is not found, and empty list is returned.
404 *
405 * @param luid
406 * the {@link Story} LUID
407 *
408 * @return the list of {@link File}s
409 *
410 * @throws IOException
411 * if the {@link Story} was not found
412 */
413 private List<File> getRelatedFiles(String luid) throws IOException {
414 List<File> files = new ArrayList<File>();
415
416 MetaData meta = getInfo(luid);
417 if (meta == null) {
418 throw new IOException("Story not found: " + luid);
211f7ddb 419 }
68e2c6d2 420
211f7ddb
NR
421 File infoFile = getStories(null).get(meta)[0];
422 File targetFile = getStories(null).get(meta)[1];
68e2c6d2 423
211f7ddb
NR
424 files.add(infoFile);
425 files.add(targetFile);
68e2c6d2 426
211f7ddb
NR
427 String readerExt = getOutputType(meta).getDefaultExtension(true);
428 String fileExt = getOutputType(meta).getDefaultExtension(false);
68e2c6d2 429
211f7ddb
NR
430 String path = targetFile.getAbsolutePath();
431 if (readerExt != null && !readerExt.equals(fileExt)) {
432 path = path.substring(0, path.length() - readerExt.length())
433 + fileExt;
434 File relatedFile = new File(path);
68e2c6d2 435
211f7ddb
NR
436 if (relatedFile.exists()) {
437 files.add(relatedFile);
68e2c6d2 438 }
211f7ddb 439 }
68e2c6d2 440
211f7ddb 441 String coverExt = "."
2a25f781
NR
442 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER)
443 .toLowerCase();
211f7ddb
NR
444 File coverFile = new File(path + coverExt);
445 if (!coverFile.exists()) {
446 coverFile = new File(path.substring(0,
447 path.length() - fileExt.length())
448 + coverExt);
449 }
450
451 if (coverFile.exists()) {
452 files.add(coverFile);
68e2c6d2
NR
453 }
454
455 return files;
456 }
457
458 /**
459 * Fill the list of stories by reading the content of the local directory
460 * {@link LocalLibrary#baseDir}.
461 * <p>
462 * Will use a cached list when possible (see
efa3c511 463 * {@link BasicLibrary#deleteInfo()}).
68e2c6d2
NR
464 *
465 * @param pg
466 * the optional {@link Progress}
467 *
9b863b20
NR
468 * @return the list of stories (for each item, the first {@link File} is the
469 * info file, the second file is the target {@link File})
68e2c6d2
NR
470 */
471 private synchronized Map<MetaData, File[]> getStories(Progress pg) {
472 if (pg == null) {
473 pg = new Progress();
474 } else {
475 pg.setMinMax(0, 100);
476 }
477
478 if (stories == null) {
479 stories = new HashMap<MetaData, File[]>();
480
481 lastId = 0;
482
483 File[] dirs = baseDir.listFiles(new FileFilter() {
211f7ddb 484 @Override
68e2c6d2
NR
485 public boolean accept(File file) {
486 return file != null && file.isDirectory();
487 }
488 });
489
b4f9071c
NR
490 if (dirs != null) {
491 Progress pgDirs = new Progress(0, 100 * dirs.length);
492 pg.addProgress(pgDirs, 100);
493
494 for (File dir : dirs) {
495 File[] infoFiles = dir.listFiles(new FileFilter() {
496 @Override
497 public boolean accept(File file) {
498 return file != null
499 && file.getPath().toLowerCase()
500 .endsWith(".info");
501 }
502 });
503
504 Progress pgFiles = new Progress(0, infoFiles.length);
505 pgDirs.addProgress(pgFiles, 100);
506 pgDirs.setName("Loading from: " + dir.getName());
507
b4f9071c
NR
508 for (File infoFile : infoFiles) {
509 pgFiles.setName(infoFile.getName());
68e2c6d2 510 try {
b4f9071c
NR
511 MetaData meta = InfoReader
512 .readMeta(infoFile, false);
b4f9071c
NR
513 try {
514 int id = Integer.parseInt(meta.getLuid());
515 if (id > lastId) {
516 lastId = id;
517 }
68e2c6d2 518
b4f9071c
NR
519 stories.put(meta, new File[] { infoFile,
520 getTargetFile(meta, infoFile) });
521 } catch (Exception e) {
522 // not normal!!
523 throw new IOException(
524 "Cannot understand the LUID of "
fd25eddc
NR
525 + infoFile + ": "
526 + meta.getLuid(), e);
b4f9071c
NR
527 }
528 } catch (IOException e) {
529 // We should not have not-supported files in the
530 // library
531 Instance.getTraceHandler().error(
532 new IOException(
533 "Cannot load file from library: "
534 + infoFile, e));
68e2c6d2 535 }
b4f9071c 536 pgFiles.add(1);
68e2c6d2 537 }
68e2c6d2 538
b4f9071c 539 pgFiles.setName(null);
14b57448
NR
540 }
541
b4f9071c 542 pgDirs.setName("Loading directories");
68e2c6d2 543 }
68e2c6d2
NR
544 }
545
b4f9071c 546 pg.done();
68e2c6d2
NR
547 return stories;
548 }
549}