+ Integrated Music Mode with UI and cmdline interface

This commit is contained in:
Reinhard Pointner 2013-01-10 18:28:46 +00:00
parent b8802bb2d3
commit ac372ca2cd
10 changed files with 322 additions and 5 deletions

View File

@ -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;

View File

@ -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<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
double max = mediaFiles.size();
@ -498,6 +505,25 @@ public class CmdlineOperations implements CmdlineInterface {
}
private List<File> renameMusic(Collection<File> 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<File, File> renameMap = new LinkedHashMap<File, File>();
// check audio files against acoustid
for (Entry<File, AudioTrack> 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);

View File

@ -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();

View File

@ -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<File, ?>> match(List<File> files, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception {
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
// check audio files against acoustid
for (Entry<File, AudioTrack> it : service.lookup(filter(files, AUDIO_FILES)).entrySet()) {
matches.add(new Match<File, AudioTrack>(it.getKey(), it.getValue()));
}
return matches;
}
}

View File

@ -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

View File

@ -2,9 +2,11 @@ help.url = http://filebot.sourceforge.net/naming.html
episode.syntax: <html><b>{</b> <b>}</b> \u2026 expression, <b>n</b> \u2026 name, <b>s</b> \u2026 season, <b>e</b> \u2026 episode, <b>t</b> \u2026 title</html>
movie.syntax: <html><b>{</b> <b>}</b> \u2026 expression, <b>n</b> \u2026 name, <b>y</b> \u2026 year</html>
music.syntax: <html><b>{</b> <b>}</b> \u2026 expression, <b>n</b> \u2026 artist, <b>t</b> \u2026 title, <b>album</b> \u2026 album</html>
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]}

View File

@ -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<String> persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode");
private static final PreferencesEntry<String> persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie");
private static final PreferencesEntry<String> persistentMusicFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.music");
private static final PreferencesEntry<String> persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en");
private static final PreferencesEntry<String> 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;
}
}
}

View File

@ -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<File, AudioTrack> lookup(Iterable<File> files) throws Exception {
Map<File, AudioTrack> results = new LinkedHashMap<File, AudioTrack>();
for (Map<String, String> 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<Map<String, String>> fpcalc(Iterable<File> files) throws IOException {
List<String> command = new ArrayList<String>();
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<Map<String, String>> results = new LinkedList<Map<String, String>>();
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<String, String>(3));
}
results.getLast().put(value[0], value[1]);
}
} finally {
scanner.close();
}
return results;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}