diff --git a/source/net/filebot/WebServices.java b/source/net/filebot/WebServices.java index 8e7e1cd1..a9209ed1 100644 --- a/source/net/filebot/WebServices.java +++ b/source/net/filebot/WebServices.java @@ -35,6 +35,7 @@ import net.filebot.web.MusicIdentificationService; import net.filebot.web.OMDbClient; import net.filebot.web.OpenSubtitlesClient; import net.filebot.web.SearchResult; +import net.filebot.web.ShooterSubtitles; import net.filebot.web.SubtitleProvider; import net.filebot.web.SubtitleSearchResult; import net.filebot.web.TMDbClient; @@ -61,6 +62,7 @@ public final class WebServices { // subtitle dbs public static final OpenSubtitlesClient OpenSubtitles = new OpenSubtitlesClientWithLocalSearch(getApiKey("opensubtitles"), getApplicationVersion()); + public static final ShooterSubtitles Shooter = new ShooterSubtitles(); // misc public static final FanartTVClient FanartTV = new FanartTVClient(Settings.getApiKey("fanart.tv")); @@ -79,8 +81,11 @@ public final class WebServices { return new SubtitleProvider[] { OpenSubtitles }; } - public static VideoHashSubtitleService[] getVideoHashSubtitleServices() { - return new VideoHashSubtitleService[] { OpenSubtitles }; + public static VideoHashSubtitleService[] getVideoHashSubtitleServices(Locale locale) { + if (locale.equals(Locale.CHINESE)) + return new VideoHashSubtitleService[] { OpenSubtitles, Shooter }; + else + return new VideoHashSubtitleService[] { OpenSubtitles }; } public static MusicIdentificationService[] getMusicIdentificationServices() { diff --git a/source/net/filebot/cli/CmdlineOperations.java b/source/net/filebot/cli/CmdlineOperations.java index 2ffe9393..64c022a8 100644 --- a/source/net/filebot/cli/CmdlineOperations.java +++ b/source/net/filebot/cli/CmdlineOperations.java @@ -713,7 +713,7 @@ public class CmdlineOperations implements CmdlineInterface { } // lookup subtitles by hash - for (VideoHashSubtitleService service : getVideoHashSubtitleServices()) { + for (VideoHashSubtitleService service : getVideoHashSubtitleServices(language.getLocale())) { if (remainingVideos.isEmpty() || (databaseFilter != null && !databaseFilter.matcher(service.getName()).matches()) || !requireLogin(service)) { continue; } diff --git a/source/net/filebot/resources/search.shooter.png b/source/net/filebot/resources/search.shooter.png new file mode 100644 index 00000000..f7156f21 Binary files /dev/null and b/source/net/filebot/resources/search.shooter.png differ diff --git a/source/net/filebot/ui/subtitle/SubtitleAutoMatchDialog.java b/source/net/filebot/ui/subtitle/SubtitleAutoMatchDialog.java index e97a2256..2039675c 100644 --- a/source/net/filebot/ui/subtitle/SubtitleAutoMatchDialog.java +++ b/source/net/filebot/ui/subtitle/SubtitleAutoMatchDialog.java @@ -60,6 +60,7 @@ import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import net.filebot.ResourceManager; +import net.filebot.WebServices; import net.filebot.mac.MacAppUtilities; import net.filebot.subtitle.SubtitleMetrics; import net.filebot.subtitle.SubtitleNaming; @@ -909,7 +910,7 @@ class SubtitleAutoMatchDialog extends JDialog { @Override public String getDescription() { - return "Exact Search"; + return service == WebServices.OpenSubtitles ? "Exact Search" : service.getName(); } @Override diff --git a/source/net/filebot/ui/subtitle/SubtitlePanel.java b/source/net/filebot/ui/subtitle/SubtitlePanel.java index 54e23885..ab367ad7 100644 --- a/source/net/filebot/ui/subtitle/SubtitlePanel.java +++ b/source/net/filebot/ui/subtitle/SubtitlePanel.java @@ -18,6 +18,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -107,7 +108,8 @@ public class SubtitlePanel extends AbstractSearchPanel> getSubtitleList(File[] videoFiles, String languageName) throws Exception { + Map> result = new HashMap>(); + for (File it : videoFiles) { + result.put(it, getSubtitleList(it, languageName)); + } + return result; + } + + /** + * @see https://docs.google.com/document/d/1ufdzy6jbornkXxsD-OGl3kgWa4P9WO5NZb6_QYZiGI0/preview + */ + public synchronized List getSubtitleList(File file, String languageName) throws Exception { + if (!LANGUAGE_CHINESE.equals(languageName) && !LANGUAGE_ENGLISH.equals(languageName)) { + throw new IllegalArgumentException("Language not supported: " + languageName); + } + if (file.length() < 8192) { + return emptyList(); + } + + URL endpoint = new URL("https://www.shooter.cn/api/subapi.php"); + Map param = new LinkedHashMap(); + param.put("filehash", computeFileHash(file)); + param.put("pathinfo", file.getPath()); + param.put("format", "json"); + param.put("lang", LANGUAGE_CHINESE.equals(languageName) ? "Chn" : "Eng"); + + Cache cache = Cache.getCache("web-datasource"); + String key = endpoint.toString() + param.toString(); + SubtitleDescriptor[] value = cache.get(key, SubtitleDescriptor[].class); + if (value != null) { + return asList(value); + } + + ByteBuffer post = WebRequest.post(endpoint, param, null); + Object response = JSONValue.parse(StandardCharsets.UTF_8.decode(post).toString()); + + List results = new ArrayList(); + String name = getNameWithoutExtension(file.getName()); + + for (JSONObject result : jsonList(response)) { + if (result == null) + continue; + + List files = jsonList(result.get("Files")); + if (files.size() == 1) { + for (JSONObject fd : jsonList(result.get("Files"))) { + String type = (String) fd.get("Ext"); + String link = (String) fd.get("Link"); + results.add(new ShooterSubtitleDescriptor(name, type, link, languageName)); + } + } + } + + cache.put(key, results.toArray(new SubtitleDescriptor[0])); + return results; + } + + @Override + public CheckResult checkSubtitle(File videoFile, File subtitleFile) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public void uploadSubtitle(Object identity, Locale locale, File videoFile, File subtitleFile) throws Exception { + throw new UnsupportedOperationException(); + } + + protected static List jsonList(final Object array) { + return new AbstractList() { + + @Override + public JSONObject get(int index) { + return (JSONObject) ((JSONArray) array).get(index); + } + + @Override + public int size() { + return array == null ? 0 : ((JSONArray) array).size(); + } + }; + } + + /** + * + * @see https://docs.google.com/document/d/1w5MCBO61rKQ6hI5m9laJLWse__yTYdRugpVyz4RzrmM/preview + */ + protected static String computeFileHash(File file) throws IOException { + List hashes = new ArrayList(); + long fileSize = file.length(); + if (fileSize < 8192) + return ""; + + long[] offset = new long[4]; + offset[3] = fileSize - 8192; + offset[2] = fileSize / 3; + offset[1] = fileSize / 3 * 2; + offset[0] = 4096; + + try (RandomAccessFile f = new RandomAccessFile(file, "r")) { + byte[] buffer = new byte[4096]; + for (int i = 0; i < 4; i++) { + f.seek(offset[i]); + int read = f.read(buffer, 0, buffer.length); + hashes.add(md5(buffer, 0, read)); + } + } + + return String.join(";", hashes); + } + + protected static String md5(byte[] input, int offset, int len) { + try { + MessageDigest hash = MessageDigest.getInstance("MD5"); + hash.update(input, offset, len); + return String.format("%032x", new BigInteger(1, hash.digest())); // as hex string + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static class ShooterSubtitleDescriptor implements SubtitleDescriptor, Serializable { + + private String name; + private String type; + private String link; + private String language; + + public ShooterSubtitleDescriptor(String name, String type, String link, String language) { + this.name = name; + this.type = type; + this.link = link; + this.language = language; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getLanguageName() { + return language; + } + + @Override + public String getType() { + return type; + } + + @Override + public ByteBuffer fetch() throws Exception { + return WebRequest.fetch(new URL(link)); + } + + @Override + public String getPath() { + return getName() + "." + getType(); + } + + @Override + public long getLength() { + return -1; + } + + @Override + public File toFile() { + return new File(getPath()); + } + + } + +}