From ac372ca2cd6ae682d5e02ca51c262f73acc5bf2c Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Thu, 10 Jan 2013 18:28:46 +0000 Subject: [PATCH] + Integrated Music Mode with UI and cmdline interface --- .../net/sourceforge/filebot/WebServices.java | 2 +- .../filebot/cli/CmdlineOperations.java | 26 ++++ .../filebot/format/MediaBindingBean.java | 31 +++++ .../ui/rename/AudioFingerprintMatcher.java | 43 ++++++ .../filebot/ui/rename/FormatDialog.java | 9 +- .../filebot/ui/rename/FormatDialog.properties | 11 ++ .../filebot/ui/rename/RenamePanel.java | 9 +- .../net/sourceforge/filebot/web/AcoustID.java | 124 ++++++++++++++++++ .../sourceforge/filebot/web/AudioTrack.java | 42 ++++++ .../filebot/web/AudioTrackFormat.java | 30 +++++ 10 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 source/net/sourceforge/filebot/ui/rename/AudioFingerprintMatcher.java create mode 100644 source/net/sourceforge/filebot/web/AcoustID.java create mode 100644 source/net/sourceforge/filebot/web/AudioTrack.java create mode 100644 source/net/sourceforge/filebot/web/AudioTrackFormat.java diff --git a/source/net/sourceforge/filebot/WebServices.java b/source/net/sourceforge/filebot/WebServices.java index 05a48b62..a71e7329 100644 --- a/source/net/sourceforge/filebot/WebServices.java +++ b/source/net/sourceforge/filebot/WebServices.java @@ -20,8 +20,8 @@ import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; -import net.sourceforge.filebot.audio.AcoustID; import net.sourceforge.filebot.media.MediaDetection; +import net.sourceforge.filebot.web.AcoustID; import net.sourceforge.filebot.web.AnidbClient; import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.FanartTV; diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index 4b658ce4..1661b18c 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -66,6 +66,8 @@ import net.sourceforge.filebot.similarity.SimilarityMetric; import net.sourceforge.filebot.subtitle.SubtitleFormat; import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.vfs.MemoryFile; +import net.sourceforge.filebot.web.AcoustID; +import net.sourceforge.filebot.web.AudioTrack; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.filebot.web.EpisodeListProvider; @@ -101,6 +103,11 @@ public class CmdlineOperations implements CmdlineInterface { return renameMovie(files, action, conflictAction, outputDir, format, getMovieIdentificationService(db), query, locale, strict); } + if (WebServices.AcoustID.getName().equalsIgnoreCase(db) || containsOnly(files, AUDIO_FILES)) { + // music mode + return renameMusic(files, action, conflictAction, outputDir, format, WebServices.AcoustID); + } + // auto-determine mode List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); double max = mediaFiles.size(); @@ -498,6 +505,25 @@ public class CmdlineOperations implements CmdlineInterface { } + private List renameMusic(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, AcoustID service) throws Exception { + // map old files to new paths by applying formatting and validating filenames + Map renameMap = new LinkedHashMap(); + + // check audio files against acoustid + for (Entry match : service.lookup(filter(files, AUDIO_FILES)).entrySet()) { + File file = match.getKey(); + AudioTrack music = match.getValue(); + String newName = (format != null) ? format.format(new MediaBindingBean(music, file)) : validateFileName(music.toString()); + + renameMap.put(file, getDestinationFile(file, newName, outputDir)); + } + + // rename movies + Analytics.trackEvent("CLI", "Rename", "Music", renameMap.size()); + return renameAll(renameMap, renameAction, conflictAction); + } + + private File getDestinationFile(File original, String newName, File outputDir) { String extension = getExtension(original); File newFile = new File(extension != null ? newName + '.' + extension : newName); diff --git a/source/net/sourceforge/filebot/format/MediaBindingBean.java b/source/net/sourceforge/filebot/format/MediaBindingBean.java index 80326aba..c9ca27f8 100644 --- a/source/net/sourceforge/filebot/format/MediaBindingBean.java +++ b/source/net/sourceforge/filebot/format/MediaBindingBean.java @@ -32,6 +32,7 @@ import net.sourceforge.filebot.hash.HashType; import net.sourceforge.filebot.media.MetaAttributes; import net.sourceforge.filebot.mediainfo.MediaInfo; import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind; +import net.sourceforge.filebot.web.AudioTrack; import net.sourceforge.filebot.web.Date; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Movie; @@ -68,6 +69,8 @@ public class MediaBindingBean { return getEpisode().getSeriesName(); if (infoObject instanceof Movie) return getMovie().getName(); + if (infoObject instanceof AudioTrack) + return getMusic().getArtist(); return null; } @@ -120,6 +123,10 @@ public class MediaBindingBean { @Define("t") public String getTitle() { + if (infoObject instanceof AudioTrack) { + return getMusic().getTitle(); + } + // single episode format if (getEpisodes().size() == 1) { return getEpisode().getTitle(); @@ -510,6 +517,24 @@ public class MediaBindingBean { } + @Define("artist") + public String getArtist() { + return getMusic().getArtist(); + } + + + @Define("title") + public String getSongTitle() { + return getMusic().getTitle(); + } + + + @Define("album") + public String getAlbum() { + return getMusic().getAlbum(); + } + + @Define("episode") public Episode getEpisode() { return (Episode) infoObject; @@ -528,6 +553,12 @@ public class MediaBindingBean { } + @Define("music") + public AudioTrack getMusic() { + return (AudioTrack) infoObject; + } + + @Define("pi") public Integer getPart() { return ((MoviePart) infoObject).getPartIndex(); diff --git a/source/net/sourceforge/filebot/ui/rename/AudioFingerprintMatcher.java b/source/net/sourceforge/filebot/ui/rename/AudioFingerprintMatcher.java new file mode 100644 index 00000000..a4e0279b --- /dev/null +++ b/source/net/sourceforge/filebot/ui/rename/AudioFingerprintMatcher.java @@ -0,0 +1,43 @@ + +package net.sourceforge.filebot.ui.rename; + + +import static net.sourceforge.filebot.MediaTypes.*; +import static net.sourceforge.tuned.FileUtilities.*; + +import java.awt.Component; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; + +import net.sourceforge.filebot.similarity.Match; +import net.sourceforge.filebot.web.AcoustID; +import net.sourceforge.filebot.web.AudioTrack; +import net.sourceforge.filebot.web.SortOrder; + + +class AudioFingerprintMatcher implements AutoCompleteMatcher { + + private AcoustID service; + + + public AudioFingerprintMatcher(AcoustID service) { + this.service = service; + } + + + @Override + public List> match(List files, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception { + List> matches = new ArrayList>(); + + // check audio files against acoustid + for (Entry it : service.lookup(filter(files, AUDIO_FILES)).entrySet()) { + matches.add(new Match(it.getKey(), it.getValue())); + } + + return matches; + } + +} diff --git a/source/net/sourceforge/filebot/ui/rename/FormatDialog.java b/source/net/sourceforge/filebot/ui/rename/FormatDialog.java index 516b8f82..8a8448d6 100644 --- a/source/net/sourceforge/filebot/ui/rename/FormatDialog.java +++ b/source/net/sourceforge/filebot/ui/rename/FormatDialog.java @@ -64,6 +64,7 @@ import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.format.BindingException; import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.MediaBindingBean; +import net.sourceforge.filebot.web.AudioTrackFormat; import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.filebot.web.MovieFormat; import net.sourceforge.tuned.DefaultThreadFactory; @@ -103,7 +104,7 @@ class FormatDialog extends JDialog { public enum Mode { - Episode, Movie; + Episode, Movie, Music; public Mode next() { if (ordinal() < values().length - 1) @@ -122,8 +123,10 @@ class FormatDialog extends JDialog { switch (this) { case Episode: return new EpisodeFormat(true, true); - default: // case Movie + case Movie: // case Movie return new MovieFormat(true, true, false); + default: + return new AudioTrackFormat(); } } @@ -228,7 +231,7 @@ class FormatDialog extends JDialog { title.setText(this.getTitle()); status.setVisible(false); - switchEditModeAction.putValue(Action.NAME, String.format("%s Format", mode.next())); + switchEditModeAction.putValue(Action.NAME, String.format("Switch to %s Format", mode.next())); updateHelpPanel(mode); // update preview to current format diff --git a/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties b/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties index b59d5ee7..ca08013b 100644 --- a/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties +++ b/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties @@ -2,9 +2,11 @@ help.url = http://filebot.sourceforge.net/naming.html episode.syntax: { } \u2026 expression, n \u2026 name, s \u2026 season, e \u2026 episode, t \u2026 title movie.syntax: { } \u2026 expression, n \u2026 name, y \u2026 year +music.syntax: { } \u2026 expression, n \u2026 artist, t \u2026 title, album \u2026 album episode.sample: Dark Angel - 3x01 - Labyrinth [2009-06-01] movie.sample: Avatar (2009) Part 1 +music.sample: Leona Lewis - I See You # basic 1.01 episode.example[0]: {n} - {s}.{e} - {t} @@ -23,3 +25,12 @@ movie.example[1]: {n} ({y}, {director}) {vf} {af} movie.example[2]: {n} {[y, certification, rating]} # normalized scene name movie.example[3]: {n.space('.')}.{y}{'.'+source}.{vc} + +# simple artist - title +music.example[0]: {n} - {t} +# simple artist - album - title +music.example[1]: {n} - {album} - {t} +# artist - title [crc32] +music.example[2]: {n} - {t} {[crc32]} +# artist - title [2ch, 128000] +music.example[3]: {n} - {t} {[af, audio.BitRate]} diff --git a/source/net/sourceforge/filebot/ui/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/rename/RenamePanel.java index b05fb137..4ebf401a 100644 --- a/source/net/sourceforge/filebot/ui/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/rename/RenamePanel.java @@ -55,6 +55,8 @@ import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.rename.RenameModel.FormattedFuture; +import net.sourceforge.filebot.web.AudioTrack; +import net.sourceforge.filebot.web.AudioTrackFormat; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.filebot.web.EpisodeListProvider; @@ -83,6 +85,7 @@ public class RenamePanel extends JComponent { private static final PreferencesEntry persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode"); private static final PreferencesEntry persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie"); + private static final PreferencesEntry persistentMusicFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.music"); private static final PreferencesEntry persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en"); private static final PreferencesEntry persistentPreferredEpisodeOrder = Settings.forPackage(RenamePanel.class).entry("rename.episode.order").defaultValue("Airdate"); @@ -258,7 +261,7 @@ public class RenamePanel extends JComponent { actionPopup.addSeparator(); actionPopup.addDescription(new JLabel("Music Mode:")); - actionPopup.add(new AutoCompleteAction("AcoustID", ResourceManager.getIcon("search.acoustid"), new AudioFingerprintMatcher(WebServices.AcoustID))); + actionPopup.add(new AutoCompleteAction(WebServices.AcoustID.getName(), WebServices.AcoustID.getIcon(), new AudioFingerprintMatcher(WebServices.AcoustID))); actionPopup.addSeparator(); actionPopup.addDescription(new JLabel("Options:")); @@ -281,6 +284,10 @@ public class RenamePanel extends JComponent { renameModel.useFormatter(Movie.class, new ExpressionFormatter(dialog.getFormat().getExpression(), MovieFormat.NameYear, Movie.class)); persistentMovieFormat.setValue(dialog.getFormat().getExpression()); break; + case Music: + renameModel.useFormatter(AudioTrack.class, new ExpressionFormatter(dialog.getFormat().getExpression(), new AudioTrackFormat(), AudioTrack.class)); + persistentMusicFormat.setValue(dialog.getFormat().getExpression()); + break; } } } diff --git a/source/net/sourceforge/filebot/web/AcoustID.java b/source/net/sourceforge/filebot/web/AcoustID.java new file mode 100644 index 00000000..590ebc92 --- /dev/null +++ b/source/net/sourceforge/filebot/web/AcoustID.java @@ -0,0 +1,124 @@ + +package net.sourceforge.filebot.web; + + +import static net.sourceforge.filebot.web.WebRequest.*; +import static net.sourceforge.tuned.FileUtilities.*; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; + +import javax.swing.Icon; + +import net.sourceforge.filebot.Cache; +import net.sourceforge.filebot.ResourceManager; + +import com.cedarsoftware.util.io.JsonReader; + + +public class AcoustID { + + private static final FloodLimit REQUEST_LIMIT = new FloodLimit(3, 1, TimeUnit.SECONDS); + + private String apikey; + + + public AcoustID(String apikey) { + this.apikey = apikey; + } + + + public String getName() { + return "AcoustID"; + } + + + public Icon getIcon() { + return ResourceManager.getIcon("search.acoustid"); + } + + + public Map lookup(Iterable files) throws Exception { + Map results = new LinkedHashMap(); + + for (Map fp : fpcalc(files)) { + results.put(new File(fp.get("FILE")), lookup(fp.get("DURATION"), fp.get("FINGERPRINT"))); + } + return results; + } + + + public AudioTrack lookup(String duration, String fingerprint) throws IOException, InterruptedException { + // http://api.acoustid.org/v2/lookup?client=8XaBELgH&meta=recordings+releasegroups+compress&duration=641&fingerprint=AQABz0qUkZK4oOfhL-CPc4e5C_wW2H2QH9uDL4cvoT8UNQ-eHtsE8cceeFJx-LiiHT-aPzhxoc-Opj_eI5d2hOFyMJRzfDk-QSsu7fBxqZDMHcfxPfDIoPWxv9C1o3yg44d_3Df2GJaUQeeR-cb2HfaPNsdxHj2PJnpwPMN3aPcEMzd-_MeB_Ej4D_CLP8ghHjkJv_jh_UDuQ8xnILwunPg6hF2R8HgzvLhxHVYP_ziJX0eKPnIE1UePMByDJyg7wz_6yELsB8n4oDmDa0Gv40hf6D3CE3_wH6HFaxCPUD9-hNeF5MfWEP3SCGym4-SxnXiGs0mRjEXD6fgl4LmKWrSChzzC33ge9PB3otyJMk-IVC6R8MTNwD9qKQ_CC8kPv4THzEGZS8GPI3x0iGVUxC1hRSizC5VzoamYDi-uR7iKPhGSI82PkiWeB_eHijvsaIWfBCWH5AjjCfVxZ1TQ3CvCTclGnEMfHbnZFA8pjD6KXwd__Cn-Y8e_I9cq6CR-4S9KLXqQcsxxoWh3eMxiHI6TIzyPv0M43YHz4yte-Cv-4D16Hv9F9C9SPUdyGtZRHV-OHEeeGD--BKcjVLOK_NCDXMfx44dzHEiOZ0Z44Rf6DH5R3uiPj4d_PKolJNyRJzyu4_CTD2WOvzjKH9GPb4cUP1Av9EuQd8fGCFee4JlRHi18xQh96NLxkCgfWFKOH6WGeoe4I3za4c5hTscTPEZTES1x8kE-9MQPjT8a8gh5fPgQZtqCFj9MDvp6fDx6NCd07bjx7MLR9AhtnFnQ70GjOcV0opmm4zpY3SOa7HiwdTtyHa6NC4e-HN-OfC5-OP_gLe2QDxfUCz_0w9l65HiPAz9-IaGOUA7-4MZ5CWFOlIfe4yUa6AiZGxf6w0fFxsjTOdC6Itbh4mGD63iPH9-RFy909XAMj7mC5_BvlDyO6kGTZKJxHUd4NDwuZUffw_5RMsde5CWkJAgXnDReNEaP6DTOQ65yaD88HoeX8fge-DSeHo9Qa8cTHc80I-_RoHxx_UHeBxrJw62Q34Kd7MEfpCcu6BLeB1ePw6OO4sOF_sHhmB504WWDZiEu8sKPpkcfCT9xfej0o0lr4T5yNJeOvjmu40w-TDmqHXmYgfFhFy_M7tD1o0cO_B2ms2j-ACEEQgQgAIwzTgAGmBIKIImNQAABwgQATAlhDGCCEIGIIM4BaBgwQBogEBIOESEIA8ARI5xAhxEFmAGAMCKAURKQQpQzRAAkCCBQEAKkQYIYIQQxCixCDADCABMAE0gpJIgyxhEDiCKCCIGAEIgJIQByAhFgGACCACMRQEyBAoxQiHiCBCFOECQFAIgAABR2QAgFjCDMA0AUMIoAIMChQghChASGEGeYEAIAIhgBSErnJPPEGWYAMgw05AhiiGHiBBBGGSCQcQgwRYJwhDDhgCSCSSEIQYwILoyAjAIigBFEUQK8gAYAQ5BCAAjkjCCAEEMZAUQAZQCjCCkpCgFMCCiIcVIAZZgilAQAiSHQECOcQAQIc4QClAHAjDDGkAGAMUoBgyhihgEChFCAAWEIEYwIJYwViAAlHCBIGEIEAEIQAoBwwgwiEBAEEEOoEwBY4wRwxAhBgAcKAESIQAwwIowRFhoBhAE + URL url = new URL("http://api.acoustid.org/v2/lookup?client=" + apikey + "&meta=recordings+releasegroups+compress&duration=" + duration + "&fingerprint=" + fingerprint); + + Cache cache = Cache.getCache("web-datasource"); + AudioTrack audioTrack = cache.get(url, AudioTrack.class); + if (audioTrack != null) + return audioTrack; + + // respect rate limit + REQUEST_LIMIT.acquirePermit(); + + String response = readAll(getReader(url.openConnection())); + Map data = JsonReader.jsonToMaps(response); + + if (!data.get("status").equals("ok")) { + throw new IOException("acoustid responded with error: " + data.get("status")); + } + + Map recording = (Map) ((List) ((Map) ((List) data.get("results")).get(0)).get("recordings")).get(0); + String artist = (String) ((Map) ((List) recording.get("artists")).get(0)).get("name"); + String title = (String) recording.get("title"); + String album = (String) ((Map) ((List) recording.get("releasegroups")).get(0)).get("title"); + audioTrack = new AudioTrack(artist, title, album); + + cache.put(url, audioTrack); + return audioTrack; + } + + + public List> fpcalc(Iterable files) throws IOException { + List command = new ArrayList(); + command.add("fpcalc"); + for (File f : files) { + command.add(f.toString()); + } + + Process process = null; + try { + process = new ProcessBuilder(command).start(); + } catch (Exception e) { + throw new IOException("Failed to exec fpcalc: " + e.getMessage()); + } + + Scanner scanner = new Scanner(process.getInputStream()); + LinkedList> results = new LinkedList>(); + + try { + while (scanner.hasNextLine()) { + String[] value = scanner.nextLine().split("=", 2); + if (value.length != 2) + continue; + + if (results.isEmpty() || results.getLast().containsKey(value[0])) { + results.addLast(new HashMap(3)); + } + results.getLast().put(value[0], value[1]); + } + } finally { + scanner.close(); + } + + return results; + } +} diff --git a/source/net/sourceforge/filebot/web/AudioTrack.java b/source/net/sourceforge/filebot/web/AudioTrack.java new file mode 100644 index 00000000..2ace3bc7 --- /dev/null +++ b/source/net/sourceforge/filebot/web/AudioTrack.java @@ -0,0 +1,42 @@ + +package net.sourceforge.filebot.web; + + +import java.io.Serializable; + + +public class AudioTrack implements Serializable { + + private String artist; + private String title; + private String album; + + + public AudioTrack(String artist, String title, String album) { + this.artist = artist; + this.title = title; + this.album = album; + } + + + public String getArtist() { + return artist; + } + + + public String getTitle() { + return title; + } + + + public String getAlbum() { + return album; + } + + + @Override + public String toString() { + return String.format("%s - %s", getArtist(), getTitle()); + } + +} diff --git a/source/net/sourceforge/filebot/web/AudioTrackFormat.java b/source/net/sourceforge/filebot/web/AudioTrackFormat.java new file mode 100644 index 00000000..aeeb78b0 --- /dev/null +++ b/source/net/sourceforge/filebot/web/AudioTrackFormat.java @@ -0,0 +1,30 @@ + +package net.sourceforge.filebot.web; + + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; + + +public class AudioTrackFormat extends Format { + + @Override + public StringBuffer format(Object obj, StringBuffer sb, FieldPosition pos) { + return sb.append(obj.toString()); + } + + + @Override + public AudioTrack parseObject(String source, ParsePosition pos) { + String[] s = source.split(" - ", 2); + if (s.length == 2) { + pos.setIndex(source.length()); + return new AudioTrack(s[0].trim(), s[1].trim(), "VA"); + } else { + pos.setErrorIndex(0); + return null; + } + } + +}