New support: YiffStar (still no logged-in content)
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
1 package be.nikiroo.fanfix;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.URL;
6 import java.util.ArrayList;
7 import java.util.Collections;
8 import java.util.HashMap;
9 import java.util.List;
10 import java.util.Map;
11 import java.util.Map.Entry;
12
13 import be.nikiroo.fanfix.bundles.Config;
14 import be.nikiroo.fanfix.data.MetaData;
15 import be.nikiroo.fanfix.data.Story;
16 import be.nikiroo.fanfix.output.BasicOutput;
17 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
18 import be.nikiroo.fanfix.supported.BasicSupport;
19 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
20 import be.nikiroo.fanfix.supported.InfoReader;
21 import be.nikiroo.utils.IOUtils;
22 import be.nikiroo.utils.Progress;
23
24 /**
25 * Manage a library of Stories: import, export, list.
26 * <p>
27 * Each {@link Story} object will be associated with a (local to the library)
28 * unique ID, the LUID, which will be used to identify the {@link Story}.
29 *
30 * @author niki
31 */
32 public class Library {
33 private File baseDir;
34 private Map<MetaData, File> stories;
35 private int lastId;
36 private OutputType text;
37 private OutputType image;
38
39 /**
40 * Create a new {@link Library} with the given backend directory.
41 *
42 * @param dir
43 * the directory where to find the {@link Story} objects
44 * @param text
45 * the {@link OutputType} to save the text-focused stories into
46 * @param image
47 * the {@link OutputType} to save the images-focused stories into
48 */
49 public Library(File dir, OutputType text, OutputType image) {
50 this.baseDir = dir;
51 this.stories = new HashMap<MetaData, File>();
52 this.lastId = 0;
53 this.text = text;
54 this.image = image;
55
56 dir.mkdirs();
57 }
58
59 /**
60 * List all the known types of stories.
61 *
62 * @return the types
63 */
64 public synchronized List<String> getTypes() {
65 List<String> list = new ArrayList<String>();
66 for (Entry<MetaData, File> entry : getStories().entrySet()) {
67 String storyType = entry.getValue().getParentFile().getName();
68 if (!list.contains(storyType)) {
69 list.add(storyType);
70 }
71 }
72
73 return list;
74 }
75
76 /**
77 * List all the stories of the given source type in the {@link Library}, or
78 * all the stories if NULL is passed as a type.
79 *
80 * @param type
81 * the type of story to retrieve, or NULL for all
82 *
83 * @return the stories
84 */
85 public synchronized List<MetaData> getList(String type) {
86 List<MetaData> list = new ArrayList<MetaData>();
87 for (Entry<MetaData, File> entry : getStories().entrySet()) {
88 String storyType = entry.getValue().getParentFile().getName();
89 if (type == null || type.equalsIgnoreCase(storyType)) {
90 list.add(entry.getKey());
91 }
92 }
93
94 Collections.sort(list);
95 return list;
96 }
97
98 /**
99 * Retrieve a {@link File} corresponding to the given {@link Story}.
100 *
101 * @param luid
102 * the Library UID of the story
103 *
104 * @return the corresponding {@link Story}
105 */
106 public synchronized MetaData getInfo(String luid) {
107 if (luid != null) {
108 for (Entry<MetaData, File> entry : getStories().entrySet()) {
109 if (luid.equals(entry.getKey().getLuid())) {
110 return entry.getKey();
111 }
112 }
113 }
114
115 return null;
116 }
117
118 /**
119 * Retrieve a {@link File} corresponding to the given {@link Story}.
120 *
121 * @param luid
122 * the Library UID of the story
123 *
124 * @return the corresponding {@link Story}
125 */
126 public synchronized File getFile(String luid) {
127 if (luid != null) {
128 for (Entry<MetaData, File> entry : getStories().entrySet()) {
129 if (luid.equals(entry.getKey().getLuid())) {
130 return entry.getValue();
131 }
132 }
133 }
134
135 return null;
136 }
137
138 /**
139 * Retrieve a specific {@link Story}.
140 *
141 * @param luid
142 * the Library UID of the story
143 * @param pg
144 * the optional progress reporter
145 *
146 * @return the corresponding {@link Story} or NULL if not found
147 */
148 public synchronized Story getStory(String luid, Progress pg) {
149 if (luid != null) {
150 for (Entry<MetaData, File> entry : getStories().entrySet()) {
151 if (luid.equals(entry.getKey().getLuid())) {
152 try {
153 SupportType type = SupportType.valueOfAllOkUC(entry
154 .getKey().getType());
155 URL url = entry.getValue().toURI().toURL();
156 if (type != null) {
157 return BasicSupport.getSupport(type).process(url,
158 pg);
159 } else {
160 throw new IOException("Unknown type: "
161 + entry.getKey().getType());
162 }
163 } catch (IOException e) {
164 // We should not have not-supported files in the
165 // library
166 Instance.syserr(new IOException(
167 "Cannot load file from library: "
168 + entry.getValue().getPath(), e));
169 }
170 }
171 }
172 }
173
174 if (pg != null) {
175 pg.setMinMax(0, 1);
176 pg.setProgress(1);
177 }
178
179 return null;
180 }
181
182 /**
183 * Import the {@link Story} at the given {@link URL} into the
184 * {@link Library}.
185 *
186 * @param url
187 * the {@link URL} to import
188 * @param pg
189 * the optional progress reporter
190 *
191 * @return the imported {@link Story}
192 *
193 * @throws IOException
194 * in case of I/O error
195 */
196 public Story imprt(URL url, Progress pg) throws IOException {
197 BasicSupport support = BasicSupport.getSupport(url);
198 if (support == null) {
199 throw new IOException("URL not supported: " + url.toString());
200 }
201
202 return save(support.process(url, pg), null);
203 }
204
205 /**
206 * Export the {@link Story} to the given target in the given format.
207 *
208 * @param luid
209 * the {@link Story} ID
210 * @param type
211 * the {@link OutputType} to transform it to
212 * @param target
213 * the target to save to
214 * @param pg
215 * the optional progress reporter
216 *
217 * @return the saved resource (the main saved {@link File})
218 *
219 * @throws IOException
220 * in case of I/O error
221 */
222 public File export(String luid, OutputType type, String target, Progress pg)
223 throws IOException {
224 Progress pgGetStory = new Progress();
225 Progress pgOut = new Progress();
226 if (pg != null) {
227 pg.setMax(2);
228 pg.addProgress(pgGetStory, 1);
229 pg.addProgress(pgOut, 1);
230 }
231
232 BasicOutput out = BasicOutput.getOutput(type, true);
233 if (out == null) {
234 throw new IOException("Output type not supported: " + type);
235 }
236
237 Story story = getStory(luid, pgGetStory);
238 if (story == null) {
239 throw new IOException("Cannot find story to export: " + luid);
240 }
241
242 return out.process(story, target, pgOut);
243 }
244
245 /**
246 * Save a {@link Story} to the {@link Library}.
247 *
248 * @param story
249 * the {@link Story} to save
250 * @param pg
251 * the optional progress reporter
252 *
253 * @return the same {@link Story}, whose LUID may have changed
254 *
255 * @throws IOException
256 * in case of I/O error
257 */
258 public Story save(Story story, Progress pg) throws IOException {
259 return save(story, null, pg);
260 }
261
262 /**
263 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
264 * correct, or NULL to get the next free one.
265 *
266 * @param story
267 * the {@link Story} to save
268 * @param luid
269 * the <b>correct</b> LUID or NULL to get the next free one
270 * @param pg
271 * the optional progress reporter
272 *
273 * @return the same {@link Story}, whose LUID may have changed
274 *
275 * @throws IOException
276 * in case of I/O error
277 */
278 public synchronized Story save(Story story, String luid, Progress pg)
279 throws IOException {
280 // Do not change the original metadata, but change the original story
281 MetaData key = story.getMeta().clone();
282 story.setMeta(key);
283
284 if (luid == null || luid.isEmpty()) {
285 getStories(); // refresh lastId if needed
286 key.setLuid(String.format("%03d", (++lastId)));
287 } else {
288 key.setLuid(luid);
289 }
290
291 getDir(key).mkdirs();
292 if (!getDir(key).exists()) {
293 throw new IOException("Cannot create library dir");
294 }
295
296 OutputType out;
297 if (key != null && key.isImageDocument()) {
298 out = image;
299 } else {
300 out = text;
301 }
302
303 BasicOutput it = BasicOutput.getOutput(out, true);
304 it.process(story, getFile(key).getPath(), pg);
305
306 // empty cache
307 stories.clear();
308
309 return story;
310 }
311
312 /**
313 * Delete the given {@link Story} from this {@link Library}.
314 *
315 * @param luid
316 * the LUID of the target {@link Story}
317 *
318 * @return TRUE if it was deleted
319 */
320 public synchronized boolean delete(String luid) {
321 boolean ok = false;
322
323 MetaData meta = getInfo(luid);
324 File file = getStories().get(meta);
325
326 if (file != null) {
327 if (file.delete()) {
328 String readerExt = getOutputType(meta)
329 .getDefaultExtension(true);
330 String fileExt = getOutputType(meta).getDefaultExtension(false);
331
332 String path = file.getAbsolutePath();
333 if (readerExt != null && !readerExt.equals(fileExt)) {
334 path = path
335 .substring(0, path.length() - readerExt.length())
336 + fileExt;
337 file = new File(path);
338 IOUtils.deltree(file);
339 }
340
341 File infoFile = new File(path + ".info");
342 if (!infoFile.exists()) {
343 infoFile = new File(path.substring(0, path.length()
344 - fileExt.length())
345 + ".info");
346 }
347 infoFile.delete();
348
349 String coverExt = "."
350 + Instance.getConfig().getString(
351 Config.IMAGE_FORMAT_COVER);
352 File coverFile = new File(path + coverExt);
353 if (!coverFile.exists()) {
354 coverFile = new File(path.substring(0, path.length()
355 - fileExt.length()));
356 }
357 coverFile.delete();
358
359 ok = true;
360 }
361
362 // clear cache
363 stories.clear();
364 }
365
366 return ok;
367 }
368
369 /**
370 * The directory (full path) where the {@link Story} related to this
371 * {@link MetaData} should be located on disk.
372 *
373 * @param key
374 * the {@link Story} {@link MetaData}
375 *
376 * @return the target directory
377 */
378 private File getDir(MetaData key) {
379 String source = key.getSource().replaceAll("[^a-zA-Z0-9._+-]", "_");
380 return new File(baseDir, source);
381 }
382
383 /**
384 * The target (full path) where the {@link Story} related to this
385 * {@link MetaData} should be located on disk.
386 *
387 * @param key
388 * the {@link Story} {@link MetaData}
389 *
390 * @return the target
391 */
392 private File getFile(MetaData key) {
393 String title = key.getTitle();
394 if (title == null) {
395 title = "";
396 }
397 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
398 return new File(getDir(key), key.getLuid() + "_" + title);
399 }
400
401 /**
402 * Return all the known stories in this {@link Library} object.
403 *
404 * @return the stories
405 */
406 private synchronized Map<MetaData, File> getStories() {
407 if (stories.isEmpty()) {
408 lastId = 0;
409
410 String ext = ".info";
411 for (File dir : baseDir.listFiles()) {
412 if (dir.isDirectory()) {
413 for (File file : dir.listFiles()) {
414 try {
415 if (file.getPath().toLowerCase().endsWith(ext)) {
416 MetaData meta = InfoReader.readMeta(file);
417 try {
418 int id = Integer.parseInt(meta.getLuid());
419 if (id > lastId) {
420 lastId = id;
421 }
422
423 // Replace .info with whatever is needed:
424 String path = file.getPath();
425 path = path.substring(0, path.length()
426 - ext.length());
427
428 String newExt = getOutputType(meta)
429 .getDefaultExtension(true);
430
431 file = new File(path + newExt);
432 //
433
434 stories.put(meta, file);
435
436 } catch (Exception e) {
437 // not normal!!
438 Instance.syserr(new IOException(
439 "Cannot understand the LUID of "
440 + file.getPath() + ": "
441 + meta.getLuid(), e));
442 }
443 }
444 } catch (IOException e) {
445 // We should not have not-supported files in the
446 // library
447 Instance.syserr(new IOException(
448 "Cannot load file from library: "
449 + file.getPath(), e));
450 }
451 }
452 }
453 }
454 }
455
456 return stories;
457 }
458
459 /**
460 * Return the {@link OutputType} for this {@link Story}.
461 *
462 * @param meta
463 * the {@link Story} {@link MetaData}
464 *
465 * @return the type
466 */
467 private OutputType getOutputType(MetaData meta) {
468 if (meta != null && meta.isImageDocument()) {
469 return image;
470 } else {
471 return text;
472 }
473 }
474 }