1 package org.musicontroller.core.jobs;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileNotFoundException;
6 import java.io.FileOutputStream;
7 import java.io.IOException;
8 import java.io.ObjectInputStream;
9 import java.io.ObjectOutputStream;
10 import java.util.HashSet;
11 import java.util.LinkedList;
12 import java.util.List;
13 import java.util.Set;
14 import java.util.regex.Matcher;
15 import java.util.regex.Pattern;
16
17 import org.apache.log4j.Logger;
18 import org.musicontroller.importer.ImporterException;
19 import org.musicontroller.importer.MP3InspectorJID3Lib;
20 import org.musicontroller.importer.MediafileInspector;
21 import org.musicontroller.importer.MusicArchiveBean;
22 import org.musicontroller.importer.MusicArchiveEntryBean;
23 import org.musicontroller.service.FileUtils;
24 import org.varienaja.util.FileOperations;
25 import org.varienaja.util.wikipedia.WikipediaSearcher;
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 public class MetadataExtractJob {
43 private static final Logger log = Logger.getLogger(MetadataExtractJob.class);
44
45 private static final String INSPECTED = "inspected";
46
47
48
49
50
51 private static List<MusicArchiveBean> _inspected;
52
53
54
55
56 public static synchronized List<MusicArchiveBean> getMusicArchiveBeanList() {
57 return _inspected;
58 }
59
60
61
62
63
64
65
66
67
68
69 public static synchronized void removeMusicArchiveBean(MusicArchiveBean archiveBean) throws ImporterException {
70 if (archiveBean==null) return;
71
72 for(MusicArchiveEntryBean entryBean: archiveBean.getEntrySet()) {
73 String filename = entryBean.getEntryName();
74 if(directoryValidForRemoval(filename)) {
75 int sepPos = filename.lastIndexOf(File.separator);
76 if(sepPos>-1) {
77 String dir = filename.substring(0,sepPos);
78 if (FileOperations.deleteDir(new File(dir))) {
79 _inspected.remove(archiveBean);
80 storeInspectedItems();
81 }
82 }
83 }
84 }
85 }
86
87
88
89
90
91
92
93
94
95 private static boolean directoryValidForRemoval(String dir) {
96 if(dir==null || dir.length()<1) {
97 return false;
98 }
99
100
101 StringBuilder regExpSb = new StringBuilder();
102 regExpSb.append("^");
103 regExpSb.append(FileUtils.getUnpackdir());
104 regExpSb.append(File.separator);
105 regExpSb.append(".+$");
106 String regExp = regExpSb.toString();
107 if(dir.matches(regExp)) {
108 return dir.indexOf("..")<0;
109 }
110 return false;
111 }
112
113 protected static synchronized File getPersistentStorage() {
114 return new File(FileUtils.getIndexdir()+File.separator+"beans.dat");
115 }
116
117
118
119
120
121 public MetadataExtractJob() {
122 initInspectedArchives();
123 }
124
125
126
127
128 @SuppressWarnings("unchecked")
129 private synchronized void initInspectedArchives() {
130 if (_inspected==null) {
131 _inspected = new LinkedList<MusicArchiveBean>();
132
133 File storage = getPersistentStorage();
134 if (storage.exists()) {
135 try {
136 ObjectInputStream in = new ObjectInputStream(new FileInputStream(storage));
137 _inspected = (List<MusicArchiveBean>) in.readObject();
138 in.close();
139 log.debug("Recovered "+_inspected.size()+" beans from persistent storage");
140 } catch (FileNotFoundException e) {
141 log.error("Error opening beanstorage: "+e);
142 } catch (IOException e) {
143 log.error("Error reading from beanstorage: "+e);
144 } catch (ClassNotFoundException e) {
145 log.error("Error decoding beanstorage: "+e);
146 }
147 }
148 }
149 }
150
151
152
153
154
155 public int execute() {
156 int result = 0;
157 File unpack = new File(FileUtils.getUnpackdir());
158 String[] files = unpack.list();
159 if (files!=null) {
160 for (String fileName : files) {
161 if (fileName.startsWith(ImportJob.READY_FOR_INSPECTION)) {
162 String fn = FileUtils.getUnpackdir()+File.separator+fileName;
163 result += inspectDirectory(fn);
164
165
166 File dirFile = new File(fn);
167 String renameddirname = FileOperations.createUniqueFilename(
168 FileUtils.getUnpackdir()+File.separator,INSPECTED);
169 File renameddir = new File(renameddirname);
170 if (dirFile.renameTo(renameddir)) {
171 fn += File.separator;
172 renameddirname += File.separator;
173
174
175 for (MusicArchiveBean mab : getMusicArchiveBeanList()) {
176 for (MusicArchiveEntryBean maeb : mab.getEntrySet()) {
177 String entryname = maeb.getEntryName();
178 if (entryname.startsWith(fn)) {
179
180 String escapedFileName = fn.replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)");
181 String replaced = escapedFileName.replaceAll("\\+", "\\\\+");
182 String newName = entryname.replaceFirst(replaced,renameddirname);
183 maeb.setEntryName(newName);
184 }
185 }
186 }
187 } else {
188 log.error("Error while renaming directory: "+fn+" to: "+renameddirname);
189 }
190 storeInspectedItems();
191 }
192 }
193 }
194
195 return result;
196 }
197
198
199
200
201
202
203
204 private int inspectDirectory(String directory) {
205 log.debug("inspection of archive "+directory+" started.");
206 int result = 0;
207 File dirFile = new File(directory);
208 String[] files = dirFile.list();
209 if (files!=null) {
210 MusicArchiveBean archiveBean = new MusicArchiveBean();
211 for (String fileName : files) {
212 if (!fileName.startsWith(".")) {
213 String fullname = directory+File.separator+fileName;
214 File inspect = new File(fullname);
215 if (inspect.isDirectory()) {
216 result += inspectDirectory(fullname);
217 } else {
218 MusicArchiveEntryBean bean = inspectFile(fullname,inspect);
219 if (bean!=null) {
220 archiveBean.addEntry(bean);
221 result++;
222 }
223 }
224 }
225 }
226
227 if (archiveBean.getEntrySet().size()>0) {
228 String name = guessName(archiveBean);
229 archiveBean.setArchiveName(name);
230 getMusicArchiveBeanList().add(archiveBean);
231 }
232 }
233 log.debug("Inspection of archive "+directory+" finished, found "+result+" tracks.");
234 return result;
235 }
236
237
238
239
240
241
242
243
244
245
246 protected String guessName(MusicArchiveBean archiveBean) {
247 String bandname = "Unknown";
248 String albumname = "Unknown";
249
250 String forbiddenCharacters = "[^ -_a-zA-Z0-9]";
251
252 Set<String> bands = new HashSet<String>();
253 Set<String> albums = new HashSet<String>();
254 if (archiveBean!=null) {
255 for (MusicArchiveEntryBean entry : archiveBean.getEntrySet()) {
256
257 if(entry.getBandName()!=null) {
258 bands.add(entry.getBandName().replaceAll(forbiddenCharacters,""));
259 }
260 if(entry.getPlaylistName()!=null) {
261 albums.add(entry.getPlaylistName().replaceAll(forbiddenCharacters,""));
262 }
263 }
264 }
265
266 if (bands.size()>0) {
267 if (bands.size()==1) {
268 bandname = bands.iterator().next();
269 } else {
270 bandname = "Various";
271 }
272 }
273 if (albums.size()>0) {
274 if (albums.size()==1) {
275 albumname = albums.iterator().next();
276 } else {
277 albumname = "Various";
278 }
279 }
280
281 return bandname + " - " + albumname;
282 }
283
284
285
286
287
288
289
290 private MusicArchiveEntryBean inspectFile(String filename, File toInspect) {
291 log.debug("Analyzing: "+filename);
292 MP3InspectorJID3Lib inspector;
293 try {
294 inspector = new MP3InspectorJID3Lib(toInspect);
295 MusicArchiveEntryBean entryBean = inspectEntry(inspector);
296 entryBean.setEntryName(filename);
297
298
299
300
301
302
303
304
305
306 return entryBean;
307 } catch (IOException e) {
308 log.error("Error analyzing file: "+e);
309 } catch (ImporterException e) {
310 log.error("Error analyzing file: "+e);
311 }
312
313 return null;
314 }
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331 private MusicArchiveEntryBean inspectEntry(MediafileInspector inspector) {
332 MusicArchiveEntryBean bean = new MusicArchiveEntryBean();
333 bean.setPlaylistName(translate(inspector.getPlaylistname()));
334 bean.setBandName(translate(inspector.getBandname()));
335 bean.setSongName(translate(inspector.getSongname()));
336 bean.setSongLength(inspector.getSonglength());
337 bean.setSongIndex(inspector.getPlaylistrow());
338
339
340
341 if (bean.getBandName()==null || bean.getBandName().equals("")
342 || bean.getBandName().equalsIgnoreCase("Various")) {
343 if (bean.getSongName()!=null && bean.getSongName().indexOf(" / ")>0) {
344 int index = bean.getSongName().indexOf(" / ");
345 String bandname = bean.getSongName().substring(0, index);
346 String songname = bean.getSongName().substring(index+3);
347 bean.setBandName(bandname);
348 bean.setSongName(songname);
349 }
350 }
351
352
353 if (bean.getSongName()==null || bean.getSongName().length()<1) {
354 Pattern pattern = Pattern.compile("^\\d+\\s+-(.+).[Mm][Pp]3");
355 Matcher matcher = pattern.matcher(inspector.getFile().getName());
356 if (matcher.find()) {
357 String songName = matcher.group(1).trim();
358 bean.setSongName(songName);
359 }
360 }
361
362
363
364 if (bean.getSongIndex()<1) {
365 String songname = inspector.getFile().getName();
366 Pattern pattern = Pattern.compile("^\\d+\\s+");
367 Matcher matcher = pattern.matcher(songname);
368 if (matcher.find()) {
369 String songIndexMatch = matcher.group().trim();
370 int songIndex = Integer.parseInt(songIndexMatch);
371 bean.setSongIndex(songIndex);
372 }
373 }
374 Set<String> keyWords = new HashSet<String>();
375
376
377
378
379
380
381
382 String id3Keyword = inspector.getKeyword();
383 String[] wikiKeywords = WikipediaSearcher.getKeywords(bean.getBandName(),bean.getPlaylistName());
384 if (wikiKeywords==null || wikiKeywords.length==0) {
385 keyWords.add(id3Keyword);
386 } else {
387 for (String wikiKeyword : wikiKeywords) {
388 keyWords.add(wikiKeyword);
389 }
390 }
391 bean.setKeywords(keyWords);
392
393 return bean;
394 }
395
396 private String translate(String str) {
397
398 if (str==null) {
399 return str;
400 }
401
402 str = str.replaceAll("'", "");
403
404
405 for (char[] chars : _CHAR_MAP) {
406 str = str.replace(chars[0], chars[1]);
407 }
408
409 return str;
410 }
411
412
413
414
415 private final char[][] _CHAR_MAP = {
416
417 {'\u00C0', 'A'}, {'\u00C1', 'A'}, {'\u00C2', 'A'}, {'\u00C3', 'A'}, {'\u00C4', 'A'}, {'\u00C5', 'A'}, {'\u00C6', 'A'},
418 {'\u00C8', 'E'}, {'\u00C9', 'E'}, {'\u00CA', 'E'}, {'\u00CB', 'E'}, {'\u00CC', 'I'}, {'\u00CD', 'I'}, {'\u00CE', 'I'},
419 {'\u00CF', 'I'}, {'\u00D2', 'O'}, {'\u00D3', 'O'}, {'\u00D4', 'O'}, {'\u00D5', 'O'}, {'\u00D6', 'O'}, {'\u00D9', 'U'},
420 {'\u00DA', 'U'}, {'\u00DB', 'U'}, {'\u00DC', 'U'}, {'\u00DF', 'B'}, {'\u00E0', 'a'}, {'\u00E1', 'a'}, {'\u00E2', 'a'},
421 {'\u00E3', 'a'}, {'\u00E4', 'a'}, {'\u00E5', 'a'}, {'\u00E6', 'a'}, {'\u00E7', 'c'}, {'\u00E8', 'e'}, {'\u00E9', 'e'},
422 {'\u00EA', 'e'}, {'\u00EB', 'e'}, {'\u00EC', 'i'}, {'\u00ED', 'i'}, {'\u00EE', 'i'}, {'\u00EF', 'i'}, {'\u00F1', 'n'},
423 {'\u00F2', 'o'}, {'\u00F3', 'o'}, {'\u00F4', 'o'}, {'\u00F5', 'o'}, {'\u00F6', 'o'}, {'\u00F8', 'o'}, {'\u00F9', 'u'},
424 {'\u00FA', 'u'}, {'\u00FB', 'u'}, {'\u00FC', 'u'},
425 };
426
427
428
429
430
431
432 private static synchronized boolean storeInspectedItems() {
433 File storage = getPersistentStorage();
434 if (_inspected.size()==0) {
435 return (storage.exists()) ? storage.delete() : true;
436 } else {
437 try {
438 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(storage));
439 out.writeObject(_inspected);
440 out.flush();
441 out.close();
442 return true;
443 } catch (FileNotFoundException e) {
444 log.error("Error opening beanstorage: "+e);
445 } catch (IOException e) {
446 log.error("Error writing to beanstorage: "+e);
447 }
448 return false;
449 }
450 }
451
452
453 }