aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata
diff options
context:
space:
mode:
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java74
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java170
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java296
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java135
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java162
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java51
6 files changed, 888 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java
new file mode 100644
index 00000000..897f39d4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java
@@ -0,0 +1,74 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import java.io.File;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+
+/**
+ * Parses meta data by guessing artist, album and song title based on the path of the file.
+ *
+ * @author Sindre Mehus
+ */
+public class DefaultMetaDataParser extends MetaDataParser {
+
+ /**
+ * Parses meta data for the given file. No guessing or reformatting is done.
+ *
+ * @param file The file to parse.
+ * @return Meta data for the file.
+ */
+ public MetaData getRawMetaData(File file) {
+ MetaData metaData = new MetaData();
+ metaData.setArtist(guessArtist(file));
+ metaData.setAlbumName(guessAlbum(file, metaData.getArtist()));
+ metaData.setTitle(guessTitle(file));
+ return metaData;
+ }
+
+ /**
+ * Updates the given file with the given meta data.
+ * This method has no effect.
+ *
+ * @param file The file to update.
+ * @param metaData The new meta data.
+ */
+ public void setMetaData(MediaFile file, MetaData metaData) {
+ }
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Always false.
+ */
+ public boolean isEditingSupported() {
+ return false;
+ }
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ public boolean isApplicable(File file) {
+ return file.isFile();
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java
new file mode 100644
index 00000000..60ae1750
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java
@@ -0,0 +1,170 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.io.InputStreamReaderThread;
+import net.sourceforge.subsonic.service.ServiceLocator;
+import net.sourceforge.subsonic.service.TranscodingService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.io.FilenameUtils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses meta data from video files using FFmpeg (http://ffmpeg.org/).
+ * <p/>
+ * Currently duration, bitrate and dimension are supported.
+ *
+ * @author Sindre Mehus
+ */
+public class FFmpegParser extends MetaDataParser {
+
+ private static final Logger LOG = Logger.getLogger(FFmpegParser.class);
+ private static final Pattern DURATION_PATTERN = Pattern.compile("Duration: (\\d+):(\\d+):(\\d+).(\\d+)");
+ private static final Pattern BITRATE_PATTERN = Pattern.compile("bitrate: (\\d+) kb/s");
+ private static final Pattern DIMENSION_PATTERN = Pattern.compile("Video.*?, (\\d+)x(\\d+)");
+ private static final Pattern PAR_PATTERN = Pattern.compile("PAR (\\d+):(\\d+)");
+
+ private TranscodingService transcodingService;
+
+ /**
+ * Parses meta data for the given music file. No guessing or reformatting is done.
+ *
+ *
+ * @param file The music file to parse.
+ * @return Meta data for the file.
+ */
+ @Override
+ public MetaData getRawMetaData(File file) {
+
+ MetaData metaData = new MetaData();
+
+ try {
+
+ File ffmpeg = new File(transcodingService.getTranscodeDirectory(), "ffmpeg");
+
+ String[] command = new String[]{ffmpeg.getAbsolutePath(), "-i", file.getAbsolutePath()};
+ Process process = Runtime.getRuntime().exec(command);
+ InputStream stdout = process.getInputStream();
+ InputStream stderr = process.getErrorStream();
+
+ // Consume stdout, we're not interested in that.
+ new InputStreamReaderThread(stdout, "ffmpeg", true).start();
+
+ // Read everything from stderr. It will contain text similar to:
+ // Input #0, avi, from 'foo.avi':
+ // Duration: 00:00:33.90, start: 0.000000, bitrate: 2225 kb/s
+ // Stream #0.0: Video: mpeg4, yuv420p, 352x240 [PAR 1:1 DAR 22:15], 29.97 fps, 29.97 tbr, 29.97 tbn, 30k tbc
+ // Stream #0.1: Audio: pcm_s16le, 44100 Hz, 2 channels, s16, 1411 kb/s
+ String[] lines = StringUtil.readLines(stderr);
+
+ Integer width = null;
+ Integer height = null;
+ Double par = 1.0;
+ for (String line : lines) {
+
+ Matcher matcher = DURATION_PATTERN.matcher(line);
+ if (matcher.find()) {
+ int hours = Integer.parseInt(matcher.group(1));
+ int minutes = Integer.parseInt(matcher.group(2));
+ int seconds = Integer.parseInt(matcher.group(3));
+ metaData.setDurationSeconds(hours * 3600 + minutes * 60 + seconds);
+ }
+
+ matcher = BITRATE_PATTERN.matcher(line);
+ if (matcher.find()) {
+ metaData.setBitRate(Integer.valueOf(matcher.group(1)));
+ }
+
+ matcher = DIMENSION_PATTERN.matcher(line);
+ if (matcher.find()) {
+ width = Integer.valueOf(matcher.group(1));
+ height = Integer.valueOf(matcher.group(2));
+ }
+
+ // PAR = Pixel Aspect Rate
+ matcher = PAR_PATTERN.matcher(line);
+ if (matcher.find()) {
+ int a = Integer.parseInt(matcher.group(1));
+ int b = Integer.parseInt(matcher.group(2));
+ if (a > 0 && b > 0) {
+ par = (double) a / (double) b;
+ }
+ }
+ }
+
+ if (width != null && height != null) {
+ width = (int) Math.round(width.doubleValue() * par);
+ metaData.setWidth(width);
+ metaData.setHeight(height);
+ }
+
+
+ } catch (Throwable x) {
+ LOG.warn("Error when parsing metadata in " + file, x);
+ }
+
+ return metaData;
+ }
+
+ /**
+ * Not supported.
+ */
+ @Override
+ public void setMetaData(MediaFile file, MetaData metaData) {
+ throw new RuntimeException("setMetaData() not supported in " + getClass().getSimpleName());
+ }
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Always false.
+ */
+ @Override
+ public boolean isEditingSupported() {
+ return false;
+ }
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ @Override
+ public boolean isApplicable(File file) {
+ String format = FilenameUtils.getExtension(file.getName()).toLowerCase();
+
+ for (String s : ServiceLocator.getSettingsService().getVideoFileTypesAsArray()) {
+ if (format.equals(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java
new file mode 100644
index 00000000..8fa7659a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java
@@ -0,0 +1,296 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import org.jaudiotagger.audio.AudioFile;
+import org.jaudiotagger.audio.AudioFileIO;
+import org.jaudiotagger.audio.AudioHeader;
+import org.jaudiotagger.tag.FieldKey;
+import org.jaudiotagger.tag.Tag;
+import org.jaudiotagger.tag.datatype.Artwork;
+import org.jaudiotagger.tag.reference.GenreTypes;
+
+import java.io.File;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.logging.LogManager;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses meta data from audio files using the Jaudiotagger library
+ * (http://www.jthink.net/jaudiotagger/)
+ *
+ * @author Sindre Mehus
+ */
+public class JaudiotaggerParser extends MetaDataParser {
+
+ private static final Logger LOG = Logger.getLogger(JaudiotaggerParser.class);
+ private static final Pattern GENRE_PATTERN = Pattern.compile("\\((\\d+)\\).*");
+ private static final Pattern TRACK_NUMBER_PATTERN = Pattern.compile("(\\d+)/\\d+");
+
+ static {
+ try {
+ LogManager.getLogManager().reset();
+ } catch (Throwable x) {
+ LOG.warn("Failed to turn off logging from Jaudiotagger.", x);
+ }
+ }
+
+ /**
+ * Parses meta data for the given music file. No guessing or reformatting is done.
+ *
+ *
+ * @param file The music file to parse.
+ * @return Meta data for the file.
+ */
+ @Override
+ public MetaData getRawMetaData(File file) {
+
+ MetaData metaData = new MetaData();
+
+ try {
+ AudioFile audioFile = AudioFileIO.read(file);
+ Tag tag = audioFile.getTag();
+ if (tag != null) {
+ metaData.setAlbumName(getTagField(tag, FieldKey.ALBUM));
+ metaData.setTitle(getTagField(tag, FieldKey.TITLE));
+ metaData.setYear(parseInteger(getTagField(tag, FieldKey.YEAR)));
+ metaData.setGenre(mapGenre(getTagField(tag, FieldKey.GENRE)));
+ metaData.setDiscNumber(parseInteger(getTagField(tag, FieldKey.DISC_NO)));
+ metaData.setTrackNumber(parseTrackNumber(getTagField(tag, FieldKey.TRACK)));
+
+ String songArtist = getTagField(tag, FieldKey.ARTIST);
+ String albumArtist = getTagField(tag, FieldKey.ALBUM_ARTIST);
+ metaData.setArtist(StringUtils.isBlank(albumArtist) ? songArtist : albumArtist);
+ }
+
+ AudioHeader audioHeader = audioFile.getAudioHeader();
+ if (audioHeader != null) {
+ metaData.setVariableBitRate(audioHeader.isVariableBitRate());
+ metaData.setBitRate((int) audioHeader.getBitRateAsNumber());
+ metaData.setDurationSeconds(audioHeader.getTrackLength());
+ }
+
+
+ } catch (Throwable x) {
+ LOG.warn("Error when parsing tags in " + file, x);
+ }
+
+ return metaData;
+ }
+
+ private String getTagField(Tag tag, FieldKey fieldKey) {
+ try {
+ return StringUtils.trimToNull(tag.getFirst(fieldKey));
+ } catch (Exception x) {
+ // Ignored.
+ return null;
+ }
+ }
+
+ /**
+ * Returns all tags supported by id3v1.
+ */
+ public static SortedSet<String> getID3V1Genres() {
+ return new TreeSet<String>(GenreTypes.getInstanceOf().getAlphabeticalValueList());
+ }
+
+ /**
+ * Sometimes the genre is returned as "(17)" or "(17)Rock", instead of "Rock". This method
+ * maps the genre ID to the corresponding text.
+ */
+ private String mapGenre(String genre) {
+ if (genre == null) {
+ return null;
+ }
+ Matcher matcher = GENRE_PATTERN.matcher(genre);
+ if (matcher.matches()) {
+ int genreId = Integer.parseInt(matcher.group(1));
+ if (genreId >= 0 && genreId < GenreTypes.getInstanceOf().getSize()) {
+ return GenreTypes.getInstanceOf().getValueForId(genreId);
+ }
+ }
+ return genre;
+ }
+
+ /**
+ * Parses the track number from the given string. Also supports
+ * track numbers on the form "4/12".
+ */
+ private Integer parseTrackNumber(String trackNumber) {
+ if (trackNumber == null) {
+ return null;
+ }
+
+ Integer result = null;
+
+ try {
+ result = new Integer(trackNumber);
+ } catch (NumberFormatException x) {
+ Matcher matcher = TRACK_NUMBER_PATTERN.matcher(trackNumber);
+ if (matcher.matches()) {
+ try {
+ result = Integer.valueOf(matcher.group(1));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ }
+
+ if (Integer.valueOf(0).equals(result)) {
+ return null;
+ }
+ return result;
+ }
+
+ private Integer parseInteger(String s) {
+ s = StringUtils.trimToNull(s);
+ if (s == null) {
+ return null;
+ }
+ try {
+ Integer result = Integer.valueOf(s);
+ if (Integer.valueOf(0).equals(result)) {
+ return null;
+ }
+ return result;
+ } catch (NumberFormatException x) {
+ return null;
+ }
+ }
+
+ /**
+ * Updates the given file with the given meta data.
+ *
+ * @param file The music file to update.
+ * @param metaData The new meta data.
+ */
+ @Override
+ public void setMetaData(MediaFile file, MetaData metaData) {
+
+ try {
+ AudioFile audioFile = AudioFileIO.read(file.getFile());
+ Tag tag = audioFile.getTagOrCreateAndSetDefault();
+
+ tag.setField(FieldKey.ARTIST, StringUtils.trimToEmpty(metaData.getArtist()));
+ tag.setField(FieldKey.ALBUM_ARTIST, StringUtils.trimToEmpty(metaData.getArtist()));
+ tag.setField(FieldKey.ALBUM, StringUtils.trimToEmpty(metaData.getAlbumName()));
+ tag.setField(FieldKey.TITLE, StringUtils.trimToEmpty(metaData.getTitle()));
+ tag.setField(FieldKey.GENRE, StringUtils.trimToEmpty(metaData.getGenre()));
+
+ Integer track = metaData.getTrackNumber();
+ if (track == null) {
+ tag.deleteField(FieldKey.TRACK);
+ } else {
+ tag.setField(FieldKey.TRACK, String.valueOf(track));
+ }
+
+ Integer year = metaData.getYear();
+ if (year == null) {
+ tag.deleteField(FieldKey.YEAR);
+ } else {
+ tag.setField(FieldKey.YEAR, String.valueOf(year));
+ }
+
+ audioFile.commit();
+
+ } catch (Throwable x) {
+ LOG.warn("Failed to update tags for file " + file, x);
+ throw new RuntimeException("Failed to update tags for file " + file + ". " + x.getMessage(), x);
+ }
+ }
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Always true.
+ */
+ @Override
+ public boolean isEditingSupported() {
+ return true;
+ }
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The music file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ @Override
+ public boolean isApplicable(File file) {
+ if (!file.isFile()) {
+ return false;
+ }
+
+ String format = FilenameUtils.getExtension(file.getName()).toLowerCase();
+
+ return format.equals("mp3") ||
+ format.equals("m4a") ||
+ format.equals("aac") ||
+ format.equals("ogg") ||
+ format.equals("flac") ||
+ format.equals("wav") ||
+ format.equals("mpc") ||
+ format.equals("mp+") ||
+ format.equals("ape") ||
+ format.equals("wma");
+ }
+
+ /**
+ * Returns whether cover art image data is available in the given file.
+ *
+ * @param file The music file.
+ * @return Whether cover art image data is available.
+ */
+ public boolean isImageAvailable(MediaFile file) {
+ try {
+ return getArtwork(file) != null;
+ } catch (Throwable x) {
+ LOG.warn("Failed to find cover art tag in " + file, x);
+ return false;
+ }
+ }
+
+ /**
+ * Returns the cover art image data embedded in the given file.
+ *
+ * @param file The music file.
+ * @return The embedded cover art image data, or <code>null</code> if not available.
+ */
+ public byte[] getImageData(MediaFile file) {
+ try {
+ return getArtwork(file).getBinaryData();
+ } catch (Throwable x) {
+ LOG.warn("Failed to find cover art tag in " + file, x);
+ return null;
+ }
+ }
+
+ private Artwork getArtwork(MediaFile file) throws Exception {
+ AudioFile audioFile = AudioFileIO.read(file.getFile());
+ Tag tag = audioFile.getTag();
+ return tag == null ? null : tag.getFirstArtwork();
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java
new file mode 100644
index 00000000..d3fa08a0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java
@@ -0,0 +1,135 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+/**
+ * Contains meta-data (song title, artist, album etc) for a music file.
+ * @author Sindre Mehus
+ */
+public class MetaData {
+
+ private Integer discNumber;
+ private Integer trackNumber;
+ private String title;
+ private String artist;
+ private String albumName;
+ private String genre;
+ private Integer year;
+ private Integer bitRate;
+ private boolean variableBitRate;
+ private Integer durationSeconds;
+ private Integer width;
+ private Integer height;
+
+ public Integer getDiscNumber() {
+ return discNumber;
+ }
+
+ public void setDiscNumber(Integer discNumber) {
+ this.discNumber = discNumber;
+ }
+
+ public Integer getTrackNumber() {
+ return trackNumber;
+ }
+
+ public void setTrackNumber(Integer trackNumber) {
+ this.trackNumber = trackNumber;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getAlbumName() {
+ return albumName;
+ }
+
+ public void setAlbumName(String albumName) {
+ this.albumName = albumName;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public boolean getVariableBitRate() {
+ return variableBitRate;
+ }
+
+ public void setVariableBitRate(boolean variableBitRate) {
+ this.variableBitRate = variableBitRate;
+ }
+
+ public Integer getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public void setDurationSeconds(Integer durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ }
+
+ public Integer getWidth() {
+ return width;
+ }
+
+ public void setWidth(Integer width) {
+ this.width = width;
+ }
+
+ public Integer getHeight() {
+ return height;
+ }
+
+ public void setHeight(Integer height) {
+ this.height = height;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java
new file mode 100644
index 00000000..2ed70acc
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java
@@ -0,0 +1,162 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import java.io.File;
+import java.util.List;
+
+import org.apache.commons.io.FilenameUtils;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.service.ServiceLocator;
+import net.sourceforge.subsonic.service.SettingsService;
+
+
+/**
+ * Parses meta data from media files.
+ *
+ * @author Sindre Mehus
+ */
+public abstract class MetaDataParser {
+
+ /**
+ * Parses meta data for the given file.
+ *
+ * @param file The file to parse.
+ * @return Meta data for the file, never null.
+ */
+ public MetaData getMetaData(File file) {
+
+ MetaData metaData = getRawMetaData(file);
+ String artist = metaData.getArtist();
+ String album = metaData.getAlbumName();
+ String title = metaData.getTitle();
+
+ if (artist == null) {
+ artist = guessArtist(file);
+ }
+ if (album == null) {
+ album = guessAlbum(file, artist);
+ }
+ if (title == null) {
+ title = guessTitle(file);
+ }
+
+ title = removeTrackNumberFromTitle(title, metaData.getTrackNumber());
+ metaData.setArtist(artist);
+ metaData.setAlbumName(album);
+ metaData.setTitle(title);
+
+ return metaData;
+ }
+
+ /**
+ * Parses meta data for the given file. No guessing or reformatting is done.
+ *
+ *
+ * @param file The file to parse.
+ * @return Meta data for the file.
+ */
+ public abstract MetaData getRawMetaData(File file);
+
+ /**
+ * Updates the given file with the given meta data.
+ *
+ * @param file The file to update.
+ * @param metaData The new meta data.
+ */
+ public abstract void setMetaData(MediaFile file, MetaData metaData);
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ public abstract boolean isApplicable(File file);
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Whether tag editing is supported.
+ */
+ public abstract boolean isEditingSupported();
+
+ /**
+ * Guesses the artist for the given file.
+ */
+ public String guessArtist(File file) {
+ File parent = file.getParentFile();
+ if (isRoot(parent)) {
+ return null;
+ }
+ File grandParent = parent.getParentFile();
+ return isRoot(grandParent) ? null : grandParent.getName();
+ }
+
+ /**
+ * Guesses the album for the given file.
+ */
+ public String guessAlbum(File file, String artist) {
+ File parent = file.getParentFile();
+ String album = isRoot(parent) ? null : parent.getName();
+ if (artist != null && album != null) {
+ album = album.replace(artist + " - ", "");
+ }
+ return album;
+ }
+
+ /**
+ * Guesses the title for the given file.
+ */
+ public String guessTitle(File file) {
+ return removeTrackNumberFromTitle(FilenameUtils.getBaseName(file.getPath()), null);
+ }
+
+ private boolean isRoot(File file) {
+ SettingsService settings = ServiceLocator.getSettingsService();
+ List<MusicFolder> folders = settings.getAllMusicFolders(false, true);
+ for (MusicFolder folder : folders) {
+ if (file.equals(folder.getPath())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes any prefixed track number from the given title string.
+ *
+ * @param title The title with or without a prefixed track number, e.g., "02 - Back In Black".
+ * @param trackNumber If specified, this is the "true" track number.
+ * @return The title with the track number removed, e.g., "Back In Black".
+ */
+ protected String removeTrackNumberFromTitle(String title, Integer trackNumber) {
+ title = title.trim();
+
+ // Don't remove numbers if true track number is given, and title does not start with it.
+ if (trackNumber != null && !title.matches("0?" + trackNumber + "[\\.\\- ].*")) {
+ return title;
+ }
+
+ String result = title.replaceFirst("^\\d{2}[\\.\\- ]+", "");
+ return result.length() == 0 ? title : result;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java
new file mode 100644
index 00000000..31b56be4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Factory for creating meta-data parsers.
+ *
+ * @author Sindre Mehus
+ */
+public class MetaDataParserFactory {
+
+ private List<MetaDataParser> parsers;
+
+ public void setParsers(List<MetaDataParser> parsers) {
+ this.parsers = parsers;
+ }
+
+ /**
+ * Returns a meta-data parser for the given file.
+ *
+ * @param file The file in question.
+ * @return An applicable parser, or <code>null</code> if no parser is found.
+ */
+ public MetaDataParser getParser(File file) {
+ for (MetaDataParser parser : parsers) {
+ if (parser.isApplicable(file)) {
+ return parser;
+ }
+ }
+ return null;
+ }
+}