1 package be
.nikiroo
.fanfix
.library
;
4 import java
.io
.FileFilter
;
5 import java
.io
.FileInputStream
;
6 import java
.io
.IOException
;
7 import java
.io
.InputStream
;
8 import java
.util
.ArrayList
;
9 import java
.util
.HashMap
;
10 import java
.util
.List
;
13 import be
.nikiroo
.fanfix
.Instance
;
14 import be
.nikiroo
.fanfix
.bundles
.Config
;
15 import be
.nikiroo
.fanfix
.data
.MetaData
;
16 import be
.nikiroo
.fanfix
.data
.Story
;
17 import be
.nikiroo
.fanfix
.output
.BasicOutput
;
18 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
19 import be
.nikiroo
.fanfix
.output
.InfoCover
;
20 import be
.nikiroo
.fanfix
.supported
.InfoReader
;
21 import be
.nikiroo
.utils
.IOUtils
;
22 import be
.nikiroo
.utils
.Image
;
23 import be
.nikiroo
.utils
.Progress
;
26 * This {@link BasicLibrary} will store the stories locally on disk.
30 public class LocalLibrary
extends BasicLibrary
{
32 private Map
<MetaData
, File
[]> stories
; // Files: [ infoFile, TargetFile ]
33 private Map
<String
, Image
> sourceCovers
;
36 private OutputType text
;
37 private OutputType image
;
40 * Create a new {@link LocalLibrary} with the given back-end directory.
43 * the directory where to find the {@link Story} objects
45 public LocalLibrary(File baseDir
) {
46 this(baseDir
, Instance
.getConfig().getString(
47 Config
.NON_IMAGES_DOCUMENT_TYPE
), Instance
.getConfig()
48 .getString(Config
.IMAGES_DOCUMENT_TYPE
), false);
52 * Create a new {@link LocalLibrary} with the given back-end directory.
55 * the directory where to find the {@link Story} objects
57 * the {@link OutputType} to use for non-image documents
59 * the {@link OutputType} to use for image documents
60 * @param defaultIsHtml
61 * if the given text or image is invalid, use HTML by default (if
62 * not, it will be INFO_TEXT/CBZ by default)
64 public LocalLibrary(File baseDir
, String text
, String image
,
65 boolean defaultIsHtml
) {
66 this(baseDir
, OutputType
.valueOfAllOkUC(text
,
67 defaultIsHtml ? OutputType
.HTML
: OutputType
.INFO_TEXT
),
68 OutputType
.valueOfAllOkUC(image
,
69 defaultIsHtml ? OutputType
.HTML
: OutputType
.CBZ
));
73 * Create a new {@link LocalLibrary} with the given back-end directory.
76 * the directory where to find the {@link Story} objects
78 * the {@link OutputType} to use for non-image documents
80 * the {@link OutputType} to use for image documents
82 public LocalLibrary(File baseDir
, OutputType text
, OutputType image
) {
83 this.baseDir
= baseDir
;
89 this.sourceCovers
= null;
95 protected List
<MetaData
> getMetas(Progress pg
) {
96 return new ArrayList
<MetaData
>(getStories(pg
).keySet());
100 public File
getFile(String luid
, Progress pg
) {
101 Instance
.getTraceHandler().trace(
102 this.getClass().getSimpleName() + ": get file for " + luid
);
105 String mess
= "no file found for ";
107 MetaData meta
= getInfo(luid
);
108 Instance
.getTraceHandler().trace("(info is: " + meta
+ ")");
110 File
[] files
= getStories(pg
).get(meta
);
112 mess
= "file retrieved for ";
116 Instance
.getTraceHandler().trace(
117 this.getClass().getSimpleName() + ": " + mess
+ luid
);
123 public Image
getCover(String luid
) {
124 MetaData meta
= getInfo(luid
);
126 File
[] files
= getStories(null).get(meta
);
128 File infoFile
= files
[0];
131 meta
= InfoReader
.readMeta(infoFile
, true);
132 return meta
.getCover();
133 } catch (IOException e
) {
134 Instance
.getTraceHandler().error(e
);
143 protected synchronized void updateInfo(MetaData meta
) {
148 protected void deleteInfo(String luid
) {
154 protected synchronized int getNextId() {
155 getStories(null); // make sure lastId is set
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
);
164 file
.getParentFile().delete();
169 protected Story
doSave(Story story
, Progress pg
) throws IOException
{
170 MetaData meta
= story
.getMeta();
172 File expectedTarget
= getExpectedFile(meta
);
173 expectedTarget
.getParentFile().mkdirs();
175 BasicOutput it
= BasicOutput
.getOutput(getOutputType(meta
), true, true);
176 it
.process(story
, expectedTarget
.getPath(), pg
);
182 protected synchronized void saveMeta(MetaData meta
, Progress pg
)
184 File newDir
= getExpectedDir(meta
.getSource());
185 if (!newDir
.exists()) {
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")) {
196 String name
= relatedFile
.getName().replaceFirst(
198 InfoCover
.writeInfo(newDir
, name
, meta
);
199 relatedFile
.delete();
200 relatedFile
.getParentFile().delete();
201 } catch (IOException e
) {
202 Instance
.getTraceHandler().error(e
);
205 relatedFile
.renameTo(new File(newDir
, relatedFile
.getName()));
206 relatedFile
.getParentFile().delete();
214 public Image
getSourceCover(String source
) {
215 if (sourceCovers
== null) {
219 if (!sourceCovers
.containsKey(source
)) {
220 sourceCovers
.put(source
, super.getSourceCover(source
));
223 return sourceCovers
.get(source
);
227 public void setSourceCover(String source
, String luid
) {
228 if (sourceCovers
== null) {
232 sourceCovers
.put(source
, getCover(luid
));
233 File cover
= new File(getExpectedDir(source
), ".cover");
235 Instance
.getCache().saveAsImage(sourceCovers
.get(source
), cover
,
237 } catch (IOException e
) {
238 Instance
.getTraceHandler().error(e
);
239 sourceCovers
.remove(source
);
244 public void imprt(BasicLibrary other
, String luid
, Progress pg
)
250 // Check if we can simply copy the files instead of the whole process
251 if (other
instanceof LocalLibrary
) {
252 LocalLibrary otherLocalLibrary
= (LocalLibrary
) other
;
254 MetaData meta
= otherLocalLibrary
.getInfo(luid
);
255 String expectedType
= ""
256 + (meta
!= null && meta
.isImageDocument() ? image
: text
);
257 if (meta
!= null && meta
.getType().equals(expectedType
)) {
258 File from
= otherLocalLibrary
.getExpectedDir(meta
.getSource());
259 File to
= this.getExpectedDir(meta
.getSource());
260 List
<File
> sources
= otherLocalLibrary
.getRelatedFiles(luid
);
261 if (!sources
.isEmpty()) {
262 pg
.setMinMax(0, sources
.size());
265 for (File source
: sources
) {
266 File target
= new File(source
.getAbsolutePath().replace(
267 from
.getAbsolutePath(), to
.getAbsolutePath()));
268 if (!source
.equals(target
)) {
269 target
.getParentFile().mkdirs();
270 InputStream in
= null;
272 in
= new FileInputStream(source
);
273 IOUtils
.write(in
, target
);
274 } catch (IOException e
) {
278 } catch (Exception ee
) {
296 super.imprt(other
, luid
, pg
);
300 * Return the {@link OutputType} for this {@link Story}.
303 * the {@link Story} {@link MetaData}
307 private OutputType
getOutputType(MetaData meta
) {
308 if (meta
!= null && meta
.isImageDocument()) {
316 * Get the target {@link File} related to the given <tt>.info</tt>
317 * {@link File} and {@link MetaData}.
322 * the <tt>.info</tt> {@link File}
324 * @return the target {@link File}
326 private File
getTargetFile(MetaData meta
, File infoFile
) {
327 // Replace .info with whatever is needed:
328 String path
= infoFile
.getPath();
329 path
= path
.substring(0, path
.length() - ".info".length());
330 String newExt
= getOutputType(meta
).getDefaultExtension(true);
332 return new File(path
+ newExt
);
336 * The target (full path) where the {@link Story} related to this
337 * {@link MetaData} should be located on disk for a new {@link Story}.
340 * the {@link Story} {@link MetaData}
344 private File
getExpectedFile(MetaData key
) {
345 String title
= key
.getTitle();
349 title
= title
.replaceAll("[^a-zA-Z0-9._+-]", "_");
350 return new File(getExpectedDir(key
.getSource()), key
.getLuid() + "_"
355 * The directory (full path) where the new {@link Story} related to this
356 * {@link MetaData} should be located on disk.
361 * @return the target directory
363 private File
getExpectedDir(String source
) {
364 String sanitizedSource
= source
.replaceAll("[^a-zA-Z0-9._+-]", "_");
365 return new File(baseDir
, sanitizedSource
);
369 * Return the list of files/directories on disk for this {@link Story}.
371 * If the {@link Story} is not found, and empty list is returned.
374 * the {@link Story} LUID
376 * @return the list of {@link File}s
378 * @throws IOException
379 * if the {@link Story} was not found
381 private List
<File
> getRelatedFiles(String luid
) throws IOException
{
382 List
<File
> files
= new ArrayList
<File
>();
384 MetaData meta
= getInfo(luid
);
386 throw new IOException("Story not found: " + luid
);
389 File infoFile
= getStories(null).get(meta
)[0];
390 File targetFile
= getStories(null).get(meta
)[1];
393 files
.add(targetFile
);
395 String readerExt
= getOutputType(meta
).getDefaultExtension(true);
396 String fileExt
= getOutputType(meta
).getDefaultExtension(false);
398 String path
= targetFile
.getAbsolutePath();
399 if (readerExt
!= null && !readerExt
.equals(fileExt
)) {
400 path
= path
.substring(0, path
.length() - readerExt
.length())
402 File relatedFile
= new File(path
);
404 if (relatedFile
.exists()) {
405 files
.add(relatedFile
);
409 String coverExt
= "."
410 + Instance
.getConfig().getString(Config
.IMAGE_FORMAT_COVER
)
412 File coverFile
= new File(path
+ coverExt
);
413 if (!coverFile
.exists()) {
414 coverFile
= new File(path
.substring(0,
415 path
.length() - fileExt
.length())
419 if (coverFile
.exists()) {
420 files
.add(coverFile
);
427 * Fill the list of stories by reading the content of the local directory
428 * {@link LocalLibrary#baseDir}.
430 * Will use a cached list when possible (see
431 * {@link BasicLibrary#deleteInfo()}).
434 * the optional {@link Progress}
436 * @return the list of stories (for each item, the first {@link File} is the info file, the
437 * second file is the target {@link File})
439 private synchronized Map
<MetaData
, File
[]> getStories(Progress pg
) {
443 pg
.setMinMax(0, 100);
446 if (stories
== null) {
447 stories
= new HashMap
<MetaData
, File
[]>();
448 sourceCovers
= new HashMap
<String
, Image
>();
452 File
[] dirs
= baseDir
.listFiles(new FileFilter() {
454 public boolean accept(File file
) {
455 return file
!= null && file
.isDirectory();
460 Progress pgDirs
= new Progress(0, 100 * dirs
.length
);
461 pg
.addProgress(pgDirs
, 100);
463 for (File dir
: dirs
) {
464 File
[] infoFiles
= dir
.listFiles(new FileFilter() {
466 public boolean accept(File file
) {
468 && file
.getPath().toLowerCase()
473 Progress pgFiles
= new Progress(0, infoFiles
.length
);
474 pgDirs
.addProgress(pgFiles
, 100);
475 pgDirs
.setName("Loading from: " + dir
.getName());
477 String source
= null;
478 for (File infoFile
: infoFiles
) {
479 pgFiles
.setName(infoFile
.getName());
481 MetaData meta
= InfoReader
482 .readMeta(infoFile
, false);
483 source
= meta
.getSource();
485 int id
= Integer
.parseInt(meta
.getLuid());
490 stories
.put(meta
, new File
[] { infoFile
,
491 getTargetFile(meta
, infoFile
) });
492 } catch (Exception e
) {
494 throw new IOException(
495 "Cannot understand the LUID of "
497 + meta
.getLuid(), e
);
499 } catch (IOException e
) {
500 // We should not have not-supported files in the
502 Instance
.getTraceHandler().error(
504 "Cannot load file from library: "
510 File cover
= new File(dir
, ".cover.png");
511 if (cover
.exists()) {
513 InputStream in
= new FileInputStream(cover
);
515 sourceCovers
.put(source
, new Image(in
));
519 } catch (IOException e
) {
520 Instance
.getTraceHandler().error(e
);
524 pgFiles
.setName(null);
527 pgDirs
.setName("Loading directories");
536 * Fix the source cover to the given story cover.
539 * the source to change
543 void setSourceCover(String source
, Image coverImage
) {
544 if (sourceCovers
== null) {
548 sourceCovers
.put(source
, coverImage
);
549 File cover
= new File(getExpectedDir(source
), ".cover");
551 Instance
.getCache().saveAsImage(sourceCovers
.get(source
), cover
,
553 } catch (IOException e
) {
554 Instance
.getTraceHandler().error(e
);
555 sourceCovers
.remove(source
);