New support: YiffStar (still no logged-in content)
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix;
2
3import java.io.File;
4import java.io.IOException;
5import java.net.URL;
6import java.util.ArrayList;
22848428 7import java.util.Collections;
08fe2e33
NR
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11import java.util.Map.Entry;
12
10d558d2 13import be.nikiroo.fanfix.bundles.Config;
08fe2e33
NR
14import be.nikiroo.fanfix.data.MetaData;
15import be.nikiroo.fanfix.data.Story;
16import be.nikiroo.fanfix.output.BasicOutput;
17import be.nikiroo.fanfix.output.BasicOutput.OutputType;
18import be.nikiroo.fanfix.supported.BasicSupport;
19import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
68686a37 20import be.nikiroo.fanfix.supported.InfoReader;
9843a5e5 21import be.nikiroo.utils.IOUtils;
3b2b638f 22import be.nikiroo.utils.Progress;
08fe2e33
NR
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 */
32public class Library {
33 private File baseDir;
34 private Map<MetaData, File> stories;
08fe2e33 35 private int lastId;
2206ef66
NR
36 private OutputType text;
37 private OutputType image;
08fe2e33
NR
38
39 /**
40 * Create a new {@link Library} with the given backend directory.
41 *
42 * @param dir
2206ef66
NR
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
08fe2e33 48 */
2206ef66 49 public Library(File dir, OutputType text, OutputType image) {
08fe2e33
NR
50 this.baseDir = dir;
51 this.stories = new HashMap<MetaData, File>();
52 this.lastId = 0;
2206ef66
NR
53 this.text = text;
54 this.image = image;
08fe2e33
NR
55
56 dir.mkdirs();
57 }
58
333f0e7b
NR
59 /**
60 * List all the known types of stories.
61 *
62 * @return the types
63 */
10d558d2 64 public synchronized List<String> getTypes() {
333f0e7b
NR
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
08fe2e33
NR
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 */
10d558d2 85 public synchronized List<MetaData> getList(String type) {
08fe2e33
NR
86 List<MetaData> list = new ArrayList<MetaData>();
87 for (Entry<MetaData, File> entry : getStories().entrySet()) {
88 String storyType = entry.getValue().getParentFile().getName();
333f0e7b 89 if (type == null || type.equalsIgnoreCase(storyType)) {
08fe2e33
NR
90 list.add(entry.getKey());
91 }
92 }
93
22848428 94 Collections.sort(list);
08fe2e33
NR
95 return list;
96 }
97
301791d3
NR
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 */
10d558d2 106 public synchronized MetaData getInfo(String luid) {
301791d3
NR
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
2206ef66
NR
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 */
10d558d2 126 public synchronized File getFile(String luid) {
2206ef66
NR
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
08fe2e33
NR
138 /**
139 * Retrieve a specific {@link Story}.
140 *
141 * @param luid
142 * the Library UID of the story
92fb0719
NR
143 * @param pg
144 * the optional progress reporter
08fe2e33 145 *
3d247bc3 146 * @return the corresponding {@link Story} or NULL if not found
08fe2e33 147 */
10d558d2 148 public synchronized Story getStory(String luid, Progress pg) {
08fe2e33
NR
149 if (luid != null) {
150 for (Entry<MetaData, File> entry : getStories().entrySet()) {
151 if (luid.equals(entry.getKey().getLuid())) {
152 try {
fe999aa4
NR
153 SupportType type = SupportType.valueOfAllOkUC(entry
154 .getKey().getType());
155 URL url = entry.getValue().toURI().toURL();
156 if (type != null) {
92fb0719
NR
157 return BasicSupport.getSupport(type).process(url,
158 pg);
fe999aa4
NR
159 } else {
160 throw new IOException("Unknown type: "
161 + entry.getKey().getType());
162 }
08fe2e33
NR
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
92fb0719
NR
174 if (pg != null) {
175 pg.setMinMax(0, 1);
176 pg.setProgress(1);
177 }
178
08fe2e33
NR
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
92fb0719
NR
188 * @param pg
189 * the optional progress reporter
08fe2e33
NR
190 *
191 * @return the imported {@link Story}
192 *
193 * @throws IOException
194 * in case of I/O error
195 */
92fb0719 196 public Story imprt(URL url, Progress pg) throws IOException {
08fe2e33
NR
197 BasicSupport support = BasicSupport.getSupport(url);
198 if (support == null) {
199 throw new IOException("URL not supported: " + url.toString());
200 }
201
92fb0719 202 return save(support.process(url, pg), null);
08fe2e33
NR
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
92fb0719
NR
214 * @param pg
215 * the optional progress reporter
08fe2e33
NR
216 *
217 * @return the saved resource (the main saved {@link File})
218 *
219 * @throws IOException
220 * in case of I/O error
221 */
92fb0719 222 public File export(String luid, OutputType type, String target, Progress pg)
08fe2e33 223 throws IOException {
bee7dffe
NR
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
08fe2e33
NR
232 BasicOutput out = BasicOutput.getOutput(type, true);
233 if (out == null) {
234 throw new IOException("Output type not supported: " + type);
235 }
236
bee7dffe 237 Story story = getStory(luid, pgGetStory);
73ce17ef
NR
238 if (story == null) {
239 throw new IOException("Cannot find story to export: " + luid);
240 }
241
bee7dffe 242 return out.process(story, target, pgOut);
08fe2e33
NR
243 }
244
245 /**
2206ef66
NR
246 * Save a {@link Story} to the {@link Library}.
247 *
248 * @param story
249 * the {@link Story} to save
bee7dffe
NR
250 * @param pg
251 * the optional progress reporter
2206ef66
NR
252 *
253 * @return the same {@link Story}, whose LUID may have changed
254 *
255 * @throws IOException
256 * in case of I/O error
257 */
bee7dffe
NR
258 public Story save(Story story, Progress pg) throws IOException {
259 return save(story, null, pg);
2206ef66
NR
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.
08fe2e33
NR
265 *
266 * @param story
267 * the {@link Story} to save
2206ef66
NR
268 * @param luid
269 * the <b>correct</b> LUID or NULL to get the next free one
bee7dffe
NR
270 * @param pg
271 * the optional progress reporter
2206ef66
NR
272 *
273 * @return the same {@link Story}, whose LUID may have changed
08fe2e33
NR
274 *
275 * @throws IOException
276 * in case of I/O error
277 */
10d558d2
NR
278 public synchronized Story save(Story story, String luid, Progress pg)
279 throws IOException {
a7d266e6 280 // Do not change the original metadata, but change the original story
301791d3 281 MetaData key = story.getMeta().clone();
a7d266e6 282 story.setMeta(key);
08fe2e33 283
2206ef66
NR
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
08fe2e33
NR
291 getDir(key).mkdirs();
292 if (!getDir(key).exists()) {
293 throw new IOException("Cannot create library dir");
294 }
295
296 OutputType out;
08fe2e33 297 if (key != null && key.isImageDocument()) {
2206ef66 298 out = image;
08fe2e33 299 } else {
2206ef66 300 out = text;
08fe2e33 301 }
2206ef66 302
08fe2e33 303 BasicOutput it = BasicOutput.getOutput(out, true);
10d558d2
NR
304 it.process(story, getFile(key).getPath(), pg);
305
306 // empty cache
307 stories.clear();
2206ef66
NR
308
309 return story;
08fe2e33
NR
310 }
311
10d558d2
NR
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()) {
9843a5e5
NR
328 String readerExt = getOutputType(meta)
329 .getDefaultExtension(true);
330 String fileExt = getOutputType(meta).getDefaultExtension(false);
10d558d2
NR
331
332 String path = file.getAbsolutePath();
9843a5e5
NR
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
10d558d2
NR
341 File infoFile = new File(path + ".info");
342 if (!infoFile.exists()) {
343 infoFile = new File(path.substring(0, path.length()
9843a5e5 344 - fileExt.length())
10d558d2
NR
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()
9843a5e5 355 - fileExt.length()));
10d558d2
NR
356 }
357 coverFile.delete();
358
359 ok = true;
360 }
361
362 // clear cache
363 stories.clear();
364 }
365
366 return ok;
367 }
368
08fe2e33
NR
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) {
a4143cd7
NR
393 String title = key.getTitle();
394 if (title == null) {
395 title = "";
396 }
397 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
08fe2e33
NR
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 */
10d558d2 406 private synchronized Map<MetaData, File> getStories() {
08fe2e33
NR
407 if (stories.isEmpty()) {
408 lastId = 0;
68686a37 409
2206ef66 410 String ext = ".info";
08fe2e33
NR
411 for (File dir : baseDir.listFiles()) {
412 if (dir.isDirectory()) {
413 for (File file : dir.listFiles()) {
414 try {
2206ef66 415 if (file.getPath().toLowerCase().endsWith(ext)) {
68686a37
NR
416 MetaData meta = InfoReader.readMeta(file);
417 try {
418 int id = Integer.parseInt(meta.getLuid());
419 if (id > lastId) {
420 lastId = id;
08fe2e33 421 }
68686a37 422
2206ef66
NR
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)
10d558d2 429 .getDefaultExtension(true);
2206ef66
NR
430
431 file = new File(path + newExt);
432 //
433
68686a37
NR
434 stories.put(meta, file);
435
436 } catch (Exception e) {
08fe2e33
NR
437 // not normal!!
438 Instance.syserr(new IOException(
68686a37
NR
439 "Cannot understand the LUID of "
440 + file.getPath() + ": "
441 + meta.getLuid(), e));
08fe2e33
NR
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 }
2206ef66
NR
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 }
08fe2e33 474}