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