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