+ Sublight support (build, client, hash, test, lib)

This commit is contained in:
Reinhard Pointner 2009-06-02 11:32:30 +00:00
parent 6593bfdbda
commit 7bb739f800
11 changed files with 566 additions and 39 deletions

View File

@ -22,15 +22,6 @@
<!-- main jar -->
<jar destfile="${dir.dist}/filebot.jar">
<fileset dir="${dir.build}" excludes="**/*Test*" />
<manifest>
<attribute name="Built-By" value="${user.name}" />
<attribute name="Built-Date" value="${today}" />
<attribute name="Application-Name" value="${title}" />
<attribute name="Application-Version" value="${version}" />
<attribute name="Class-Path" value="args4j.jar miglayout.jar glazedlists.jar nekohtml.jar xercesImpl.jar ehcache.jar simmetrics.jar xmlrpc-client.jar" />
<attribute name="Main-Class" value="net.sourceforge.filebot.Main" />
</manifest>
</jar>
<!-- extra jar containing all the unit tests -->
@ -45,10 +36,16 @@
<mkdir dir="${dir.dist}/fatjar" />
<jar destfile="${dir.dist}/fatjar/FileBot.jar" filesetmanifest="merge" duplicate="fail">
<manifest>
<attribute name="Built-By" value="${user.name}" />
<attribute name="Built-Date" value="${today}" />
<attribute name="Application-Name" value="${title}" />
<attribute name="Application-Version" value="${version}" />
<attribute name="Main-Class" value="net.sourceforge.filebot.Main" />
</manifest>
<zipfileset src="${dir.dist}/filebot.jar">
<include name="**/*" />
</zipfileset>
<!-- include build -->
<fileset dir="${dir.build}" excludes="**/*Test*" />
<!-- include libs -->
<zipfileset src="${dir.lib}/xercesImpl.jar">
@ -96,6 +93,10 @@
<zipfileset src="${dir.lib}/js.jar">
<include name="org/mozilla/**" />
</zipfileset>
<zipfileset src="${dir.lib}/sublight-ws.jar">
<include name="net/sublight/webservice/**" />
</zipfileset>
</jar>
</target>

BIN
lib/sublight-ws.jar Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

View File

@ -62,8 +62,8 @@ public class IMDbClient implements EpisodeListProvider {
List<SearchResult> results = new ArrayList<SearchResult>(nodes.size());
for (Node node : nodes) {
String name = removeQuotationMarks(node.getTextContent().trim());
String year = node.getNextSibling().getTextContent().trim();
String name = normalizeName(node.getTextContent().trim());
String year = node.getNextSibling().getTextContent().trim().replaceAll("\\D+", ""); // remove non-number characters
String href = getAttribute("href", node);
results.add(new MovieDescriptor(name, Integer.parseInt(year), getImdbId(href)));
@ -71,7 +71,7 @@ public class IMDbClient implements EpisodeListProvider {
// we might have been redirected to the movie page
if (results.isEmpty()) {
String name = removeQuotationMarks(selectString("//H1/text()", dom));
String name = normalizeName(selectString("//H1/text()", dom));
String year = selectString("//H1//A", dom);
String url = selectString("//LINK[@rel='canonical']/@href", dom);
@ -86,7 +86,7 @@ public class IMDbClient implements EpisodeListProvider {
public List<Episode> getEpisodeList(SearchResult searchResult) throws IOException, SAXException {
Document dom = getHtmlDocument(openConnection(getEpisodeListLink(searchResult).toURL()));
String seriesName = removeQuotationMarks(selectString("//H1/A", dom));
String seriesName = normalizeName(selectString("//H1/A", dom));
List<Node> nodes = selectNodes("//TABLE//H3/A[preceding-sibling::text()]", dom);
@ -129,8 +129,9 @@ public class IMDbClient implements EpisodeListProvider {
}
protected String removeQuotationMarks(String name) {
return name.replaceAll("^\"|\"$", "");
protected String normalizeName(String name) {
// remove quotation marks
return name.replaceAll("\"", "");
}

View File

@ -0,0 +1,252 @@
package net.sourceforge.filebot.web;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.swing.Icon;
import javax.xml.ws.Holder;
import javax.xml.ws.WebServiceException;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.tuned.Timer;
import net.sublight.webservice.ArrayOfGenre;
import net.sublight.webservice.ArrayOfIMDB;
import net.sublight.webservice.ArrayOfRelease;
import net.sublight.webservice.ArrayOfSubtitle;
import net.sublight.webservice.ArrayOfSubtitleLanguage;
import net.sublight.webservice.Genre;
import net.sublight.webservice.IMDB;
import net.sublight.webservice.Release;
import net.sublight.webservice.Subtitle;
import net.sublight.webservice.SubtitleLanguage;
import net.sublight.webservice.SubtitlesAPI2;
import net.sublight.webservice.SubtitlesAPI2Soap;
public class SublightSubtitleClient implements SubtitleProvider {
private static final String iid = "42cc1701-3752-49e2-a148-332960073452";
private final String clientInfo;
private final SubtitlesAPI2Soap webservice;
private String session;
public SublightSubtitleClient(String clientInfo) {
this.clientInfo = clientInfo;
this.webservice = new SubtitlesAPI2().getSubtitlesAPI2Soap();
}
@Override
public String getName() {
return "Sublight";
}
@Override
public Icon getIcon() {
return ResourceManager.getIcon("search.sublight");
}
@Override
public List<SearchResult> search(String query) throws WebServiceException {
// require login
login();
Holder<ArrayOfIMDB> response = new Holder<ArrayOfIMDB>();
Holder<String> error = new Holder<String>();
webservice.findIMDB(query, null, null, response, error);
// abort if something went wrong
checkError(error);
List<SearchResult> results = new ArrayList<SearchResult>();
if (response.value != null) {
for (IMDB imdb : response.value.getIMDB()) {
// remove classifier (e.g. tt0436992 -> 0436992)
int id = Integer.parseInt(imdb.getId().substring(2));
results.add(new MovieDescriptor(imdb.getTitle(), imdb.getYear(), id));
}
}
return results;
}
@Override
public List<SubtitleDescriptor> getSubtitleList(SearchResult searchResult, String languageName) throws WebServiceException {
MovieDescriptor movie = (MovieDescriptor) searchResult;
List<SubtitleDescriptor> subtitles = new ArrayList<SubtitleDescriptor>();
// retrieve subtitles by name and year
for (Subtitle subtitle : getSubtitleList(null, movie.getName(), movie.getYear(), languageName)) {
subtitles.add(new SublightSubtitleDescriptor(subtitle));
}
return subtitles;
}
public List<SubtitleDescriptor> getSubtitleList(File videoFile, String languageName) throws WebServiceException, IOException {
List<SubtitleDescriptor> subtitles = new ArrayList<SubtitleDescriptor>();
// retrieve subtitles by video hash
for (Subtitle subtitle : getSubtitleList(SublightVideoHasher.computeHash(videoFile), null, null, languageName)) {
// only keep linked subtitles
if (subtitle.isIsLinked()) {
subtitles.add(new SublightSubtitleDescriptor(subtitle));
}
}
return subtitles;
}
protected List<Subtitle> getSubtitleList(String videoHash, String name, Integer year, String languageName) throws WebServiceException {
// require login
login();
// given language or all languages
ArrayOfSubtitleLanguage languages = new ArrayOfSubtitleLanguage();
if (languageName != null) {
// given language
languages.getSubtitleLanguage().add(getSubtitleLanguage(languageName));
} else {
// all languages
Collections.addAll(languages.getSubtitleLanguage(), SubtitleLanguage.values());
}
// all genres
ArrayOfGenre genres = new ArrayOfGenre();
Collections.addAll(genres.getGenre(), Genre.values());
// response holders
Holder<ArrayOfSubtitle> subtitles = new Holder<ArrayOfSubtitle>();
Holder<ArrayOfRelease> releases = new Holder<ArrayOfRelease>();
Holder<String> error = new Holder<String>();
webservice.searchSubtitles3(session, videoHash, name, year, null, null, languages, genres, null, null, null, subtitles, releases, null, error);
// abort if something went wrong
checkError(error);
// return empty list if response is empty
if (subtitles.value == null) {
return Collections.emptyList();
}
// map all release names by subtitle id
if (releases.value != null) {
Map<String, String> releaseNameBySubtitleID = new HashMap<String, String>();
// map release names by subtitle id
for (Release release : releases.value.getRelease()) {
releaseNameBySubtitleID.put(release.getSubtitleID(), release.getName());
}
// set release names
for (Subtitle subtitle : subtitles.value.getSubtitle()) {
subtitle.setRelease(releaseNameBySubtitleID.get(subtitle.getSubtitleID()));
}
}
return subtitles.value.getSubtitle();
}
protected SubtitleLanguage getSubtitleLanguage(String languageName) {
for (SubtitleLanguage language : SubtitleLanguage.values()) {
if (language.value().equalsIgnoreCase(languageName))
return language;
}
// special language name handling
if (languageName.equalsIgnoreCase("Brazilian"))
return SubtitleLanguage.PORTUGUESE_BRAZIL;
if (languageName.equalsIgnoreCase("Bosnian"))
return SubtitleLanguage.BOSNIAN_LATIN;
if (languageName.equalsIgnoreCase("Serbian"))
return SubtitleLanguage.SERBIAN_LATIN;
// unkown language
throw new IllegalArgumentException("Illegal language: " + languageName);
}
@Override
public URI getSubtitleListLink(SearchResult searchResult, String languageName) {
return null;
}
protected synchronized void login() throws WebServiceException {
if (session == null) {
Holder<String> session = new Holder<String>();
Holder<String> error = new Holder<String>();
webservice.logInAnonymous3(clientInfo, iid, session, null, error);
// abort if something went wrong
checkError(error);
// start session
this.session = session.value;
}
// reset timer
logoutTimer.set(10, TimeUnit.MINUTES, true);
}
protected synchronized void logout() throws WebServiceException {
if (session != null) {
Holder<String> error = new Holder<String>();
webservice.logOut(session, null, error);
// abort if something went wrong
checkError(error);
// stop session
this.session = null;
// cancel timer
logoutTimer.cancel();
}
}
protected void checkError(Holder<?> error) throws WebServiceException {
if (error.value != null) {
throw new WebServiceException("Login failed: " + error.value);
}
}
protected final Timer logoutTimer = new Timer() {
@Override
public void run() {
logout();
}
};
}

View File

@ -0,0 +1,60 @@
package net.sourceforge.filebot.web;
import net.sourceforge.tuned.DownloadTask;
import net.sublight.webservice.Subtitle;
public class SublightSubtitleDescriptor implements SubtitleDescriptor {
private final Subtitle subtitle;
public SublightSubtitleDescriptor(Subtitle subtitle) {
this.subtitle = subtitle;
}
@Override
public String getName() {
// use release name by default
String releaseName = subtitle.getRelease();
if (releaseName == null || releaseName.isEmpty()) {
// create name from subtitle information (name, season, episode, ...)
String season = subtitle.getSeason() != null ? subtitle.getSeason().toString() : null;
String episode = subtitle.getEpisode() != null ? subtitle.getEpisode().toString() : null;
return EpisodeFormat.getInstance().format(new Episode(subtitle.getTitle(), season, episode, null));
}
return releaseName;
}
@Override
public String getArchiveType() {
return subtitle.getSubtitleType().value().toLowerCase();
}
@Override
public String getLanguageName() {
return subtitle.getLanguage().value();
}
@Override
public DownloadTask createDownloadTask() {
// TODO support
return new DownloadTask(null);
}
@Override
public String toString() {
return String.format("%s [%s]", getName(), getLanguageName());
}
}

View File

@ -0,0 +1,124 @@
package net.sourceforge.filebot.web;
import static java.lang.Math.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.security.MessageDigest;
import java.util.Formatter;
import java.util.concurrent.TimeUnit;
import net.sourceforge.filebot.mediainfo.MediaInfo;
import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind;
/**
* Compute special hash used by <a href="http://www.subtitles-on.net">Sublight</a> to identify video files.
*
* <pre>
* The hash is divided into 5 sections:
* 1 byte : reserved
* 2 bytes: video duration in seconds
* 6 bytes: file size in bytes
* 16 bytes: MD5 hash of the first 5 MB
* 1 byte: control byte, sum of all other bytes
* </pre>
*/
public final class SublightVideoHasher {
public static String computeHash(File file) throws IOException {
byte[][] hash = new byte[4][];
// 1 byte = 0 (reserved)
hash[0] = new byte[] { 0 };
// 2 bytes (video duration in seconds)
hash[1] = getTrailingBytes(getDuration(file, TimeUnit.SECONDS), 2);
// 6 bytes (file size in bytes)
hash[2] = getTrailingBytes(file.length(), 6);
// 16 bytes (md5 hash of the first 5 MB)
hash[3] = getHeadMD5(file, 5 * 1024 * 1024);
// format and sum
Formatter hex = new Formatter(new StringBuilder(52));
byte sum = 0;
for (byte[] group : hash) {
for (byte b : group) {
hex.format("%02x", b);
sum += b;
}
}
// 1 byte (control byte)
hex.format("%02x", sum);
// done
return hex.out().toString();
}
protected static byte[] getTrailingBytes(long value, int n) {
byte[] bytes = BigInteger.valueOf(value).toByteArray();
// bytes will be initialized with 0
byte[] trailingBytes = new byte[n];
// copy the least significant n bytes to the new array
System.arraycopy(bytes, max(0, bytes.length - n), trailingBytes, max(0, n - bytes.length), min(n, bytes.length));
return trailingBytes;
}
protected static long getDuration(File file, TimeUnit unit) throws IOException {
try {
MediaInfo mediaInfo = new MediaInfo();
if (!mediaInfo.open(file))
throw new IllegalArgumentException("Failed to open file: " + file);
// get media info
String duration = mediaInfo.get(StreamKind.General, 0, "Duration");
// close handle
mediaInfo.close();
// convert from milliseconds to given unit
return unit.convert(Long.parseLong(duration), TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new IOException("Failed to get video duration", e);
}
}
protected static byte[] getHeadMD5(File file, long chunkSize) throws IOException {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
FileChannel channel = new FileInputStream(file).getChannel();
try {
// calculate md5
md5.update(channel.map(MapMode.READ_ONLY, 0, min(channel.size(), chunkSize)));
} finally {
// close channel
channel.close();
}
return md5.digest();
} catch (Exception e) {
throw new IOException("Failed to calculate md5 hash", e);
}
}
}

View File

@ -87,8 +87,8 @@ public class IMDbClientTest {
@Test
public void removeQuotationMarks() throws Exception {
assertEquals("test", imdb.removeQuotationMarks("\"test\""));
assertEquals("test", imdb.normalizeName("\"test\""));
assertEquals("inner \"quotation marks\"", imdb.removeQuotationMarks("\"inner \"quotation marks\"\""));
assertEquals("inner \"quotation marks\"", imdb.normalizeName("\"inner \"quotation marks\"\""));
}
}

View File

@ -9,7 +9,6 @@ import java.io.FileInputStream;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@ -17,7 +16,6 @@ import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
@Ignore("No test data")
public class OpenSubtitlesHasherTest {
private String expectedHash;

View File

@ -0,0 +1,91 @@
package net.sourceforge.filebot.web;
import static org.junit.Assert.*;
import java.util.List;
import net.sublight.webservice.Subtitle;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class SublightSubtitleClientTest {
private static SublightSubtitleClient client = new SublightSubtitleClient("Test;0.0");
@BeforeClass
public static void login() {
// login manually
client.login();
}
@Test
public void search() {
List<SearchResult> list = client.search("babylon 5");
MovieDescriptor sample = (MovieDescriptor) list.get(0);
// check sample entry
assertEquals("Babylon 5", sample.getName());
assertEquals(105946, sample.getImdbId());
// check size
assertEquals(8, list.size());
}
@Test
public void getSubtitleListEnglish() {
List<SubtitleDescriptor> list = client.getSubtitleList(new MovieDescriptor("Heroes", 2006, 813715), "English");
SubtitleDescriptor sample = list.get(0);
assertTrue(sample.getName().startsWith("Heroes"));
assertEquals("English", sample.getLanguageName());
// check size
assertTrue(list.size() > 45);
}
@Test
public void getSubtitleListAllLanguages() {
List<SubtitleDescriptor> list = client.getSubtitleList(new MovieDescriptor("Babylon 5", 1994, 105946), null);
SubtitleDescriptor sample = list.get(0);
assertEquals("Babylon.5.S01E01.Midnight.on.the.Firing.Line.AC3.DVDRip.DivX-AMC", sample.getName());
assertEquals("Slovenian", sample.getLanguageName());
// check size
assertTrue(list.size() > 45);
}
@Test
public void getSubtitleListVideoHash() {
List<Subtitle> list = client.getSubtitleList("000a20000045eacfebd3c2c83bfb4ea1598b14e9be7db38316fd", null, null, "English");
Subtitle sample = list.get(0);
assertEquals("Terminator: The Sarah Connor Chronicles", sample.getTitle());
assertEquals(2, sample.getSeason(), 0);
assertEquals(22, sample.getEpisode(), 0);
assertEquals("Terminator.The.Sarah.Connor.Chronicles.S02E22.HDTV.XviD-2HD", sample.getRelease());
assertTrue(sample.isIsLinked());
}
@AfterClass
public static void logout() {
// logout manually
client.logout();
}
}

View File

@ -8,7 +8,7 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, IMDbClientTest.class, SubsceneSubtitleClientTest.class, SubtitleSourceClientTest.class, OpenSubtitlesHasherTest.class })
@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, IMDbClientTest.class, SubsceneSubtitleClientTest.class, SubtitleSourceClientTest.class, SublightSubtitleClientTest.class })
public class WebTestSuite {
}