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