From 0ecc4e03fbb75c6571d4a3e85f48443261b9e172 Mon Sep 17 00:00:00 2001 From: zerratar Date: Tue, 10 Apr 2018 23:34:53 +0200 Subject: [PATCH 1/2] Start adding support for opensubtitles.org now that the useragent has been accepted --- SubSync.sln | 14 + src/SubSync/Core/AuthCredentials.cs | 16 + .../DownloadQuotaReachedException.cs | 8 + .../NestedArchiveNotSupportedException.cs | 12 + .../RequestQuotaReachedException.cs | 8 + .../Exceptions/SubtitleNotFoundException.cs | 8 + src/SubSync/Core/FallbackSubtitleProvider.cs | 27 +- .../Core/FileBasedCredentialsProvider.cs | 62 ++ src/SubSync/Core/IAuthCredentialProvider.cs | 7 + src/SubSync/Core/IFileSystemWatcher.cs | 2 +- src/SubSync/Core/IStatusReporter.cs | 20 + src/SubSync/Core/ISubtitleProvider.cs | 3 +- src/SubSync/Core/IWorkerProvider.cs | 2 +- src/SubSync/Core/IWorkerQueue.cs | 5 +- src/SubSync/Core/QueueProcessReporter.cs | 51 ++ src/SubSync/Core/QueueProcessResult.cs | 16 + src/SubSync/Core/SubtitleLanguage.cs | 537 ++++++++++++++++++ src/SubSync/Core/SubtitleProviderBase.cs | 98 +++- src/SubSync/Core/SubtitleSynchronizer.cs | 40 +- src/SubSync/Core/Worker.cs | 101 ++-- src/SubSync/Core/WorkerProvider.cs | 15 +- src/SubSync/Core/WorkerQueue.cs | 50 +- src/SubSync/Core/WorkerStatus.cs | 14 + src/SubSync/Core/XmlRpc/IXmlRpcObjectValue.cs | 7 + src/SubSync/Core/XmlRpc/XmlRpcArray.cs | 25 + src/SubSync/Core/XmlRpc/XmlRpcDouble.cs | 27 + src/SubSync/Core/XmlRpc/XmlRpcInt.cs | 27 + src/SubSync/Core/XmlRpc/XmlRpcMember.cs | 29 + src/SubSync/Core/XmlRpc/XmlRpcObject.cs | 140 +++++ src/SubSync/Core/XmlRpc/XmlRpcRoot.cs | 48 ++ src/SubSync/Core/XmlRpc/XmlRpcString.cs | 27 + src/SubSync/Core/XmlRpc/XmlRpcStruct.cs | 27 + src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs | 12 + src/SubSync/Program.cs | 64 +-- src/SubSync/SubSync.csproj | 10 +- src/SubSync/SubSync.csproj.DotSettings | 2 + .../SubtitleProviders/OpenSubtitles.cs | 456 ++++++++++++++- src/SubSync/SubtitleProviders/Subscene.cs | 4 - src/SubSync/opensubtitles.auth | 2 + tests/SubSync.Tests/SubSync.Tests.csproj | 19 + tests/SubSync.Tests/UnitTest1.cs | 32 ++ 41 files changed, 1915 insertions(+), 159 deletions(-) create mode 100644 src/SubSync/Core/AuthCredentials.cs create mode 100644 src/SubSync/Core/Exceptions/DownloadQuotaReachedException.cs create mode 100644 src/SubSync/Core/Exceptions/NestedArchiveNotSupportedException.cs create mode 100644 src/SubSync/Core/Exceptions/RequestQuotaReachedException.cs create mode 100644 src/SubSync/Core/Exceptions/SubtitleNotFoundException.cs create mode 100644 src/SubSync/Core/FileBasedCredentialsProvider.cs create mode 100644 src/SubSync/Core/IAuthCredentialProvider.cs create mode 100644 src/SubSync/Core/IStatusReporter.cs create mode 100644 src/SubSync/Core/QueueProcessReporter.cs create mode 100644 src/SubSync/Core/QueueProcessResult.cs create mode 100644 src/SubSync/Core/SubtitleLanguage.cs create mode 100644 src/SubSync/Core/WorkerStatus.cs create mode 100644 src/SubSync/Core/XmlRpc/IXmlRpcObjectValue.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcArray.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcDouble.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcInt.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcMember.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcObject.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcRoot.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcString.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcStruct.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs create mode 100644 src/SubSync/opensubtitles.auth create mode 100644 tests/SubSync.Tests/SubSync.Tests.csproj create mode 100644 tests/SubSync.Tests/UnitTest1.cs diff --git a/SubSync.sln b/SubSync.sln index 5166bc3..b8bc3cb 100644 --- a/SubSync.sln +++ b/SubSync.sln @@ -5,6 +5,12 @@ VisualStudioVersion = 15.0.27428.2011 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SubSync", "src\SubSync\SubSync.csproj", "{A1B8C57E-2B99-4D21-A35E-D050F3BA279B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C182B254-972A-4CB2-8002-1653E0F24323}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7D3E5130-833D-4426-9E7C-7D91CBA217B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SubSync.Tests", "tests\SubSync.Tests\SubSync.Tests.csproj", "{71C95A54-2D86-4AC9-AE79-05CF93E063EA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,10 +21,18 @@ Global {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Release|Any CPU.Build.0 = Release|Any CPU + {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A1B8C57E-2B99-4D21-A35E-D050F3BA279B} = {C182B254-972A-4CB2-8002-1653E0F24323} + {71C95A54-2D86-4AC9-AE79-05CF93E063EA} = {7D3E5130-833D-4426-9E7C-7D91CBA217B3} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1EBFA09F-C84B-4414-AAF2-998BD08F6A97} EndGlobalSection diff --git a/src/SubSync/Core/AuthCredentials.cs b/src/SubSync/Core/AuthCredentials.cs new file mode 100644 index 0000000..7792169 --- /dev/null +++ b/src/SubSync/Core/AuthCredentials.cs @@ -0,0 +1,16 @@ +namespace SubSync +{ + internal struct AuthCredentials + { + public readonly string Username; + public readonly string Password; + + public AuthCredentials(string username, string password) + { + Username = username; + Password = password; + } + + public bool IsEmpty => string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(Password); + } +} \ No newline at end of file diff --git a/src/SubSync/Core/Exceptions/DownloadQuotaReachedException.cs b/src/SubSync/Core/Exceptions/DownloadQuotaReachedException.cs new file mode 100644 index 0000000..914fb7b --- /dev/null +++ b/src/SubSync/Core/Exceptions/DownloadQuotaReachedException.cs @@ -0,0 +1,8 @@ +using System; + +namespace SubSync +{ + internal class DownloadQuotaReachedException : Exception + { + } +} \ No newline at end of file diff --git a/src/SubSync/Core/Exceptions/NestedArchiveNotSupportedException.cs b/src/SubSync/Core/Exceptions/NestedArchiveNotSupportedException.cs new file mode 100644 index 0000000..00db841 --- /dev/null +++ b/src/SubSync/Core/Exceptions/NestedArchiveNotSupportedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace SubSync +{ + internal class NestedArchiveNotSupportedException : Exception + { + public NestedArchiveNotSupportedException(string filename) + : base($"Downloaded archive, '{filename}' @red@contain another archive within it and cannot properly be extracted. Archive kept for manual labor.") + { + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/Exceptions/RequestQuotaReachedException.cs b/src/SubSync/Core/Exceptions/RequestQuotaReachedException.cs new file mode 100644 index 0000000..8661193 --- /dev/null +++ b/src/SubSync/Core/Exceptions/RequestQuotaReachedException.cs @@ -0,0 +1,8 @@ +using System; + +namespace SubSync +{ + internal class RequestQuotaReachedException : Exception + { + } +} \ No newline at end of file diff --git a/src/SubSync/Core/Exceptions/SubtitleNotFoundException.cs b/src/SubSync/Core/Exceptions/SubtitleNotFoundException.cs new file mode 100644 index 0000000..6bcc0ff --- /dev/null +++ b/src/SubSync/Core/Exceptions/SubtitleNotFoundException.cs @@ -0,0 +1,8 @@ +using System; + +namespace SubSync +{ + internal class SubtitleNotFoundException : Exception + { + } +} \ No newline at end of file diff --git a/src/SubSync/Core/FallbackSubtitleProvider.cs b/src/SubSync/Core/FallbackSubtitleProvider.cs index 80a45c1..4668592 100644 --- a/src/SubSync/Core/FallbackSubtitleProvider.cs +++ b/src/SubSync/Core/FallbackSubtitleProvider.cs @@ -4,11 +4,12 @@ namespace SubSync { - internal class FallbackSubtitleProvider : ISubtitleProvider + internal class FallbackSubtitleProvider : ISubtitleProvider, IDisposable { private readonly ConcurrentDictionary providerCache = new ConcurrentDictionary(); private readonly ISubtitleProvider[] _providers; private readonly int MaxRetryCount = 3; + private bool disposed; public FallbackSubtitleProvider(params ISubtitleProvider[] providers) { @@ -37,5 +38,29 @@ public async Task GetAsync(string name, string outputDirectory) return await this.GetAsync(name, outputDirectory); } } + + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + foreach (var provider in this._providers) + { + try + { + if (provider is IDisposable disposable) + { + disposable.Dispose(); + } + } + catch + { + // ignored + } + } + } } } \ No newline at end of file diff --git a/src/SubSync/Core/FileBasedCredentialsProvider.cs b/src/SubSync/Core/FileBasedCredentialsProvider.cs new file mode 100644 index 0000000..cd2c38e --- /dev/null +++ b/src/SubSync/Core/FileBasedCredentialsProvider.cs @@ -0,0 +1,62 @@ +using System; + +namespace SubSync +{ + internal class FileBasedCredentialsProvider : IAuthCredentialProvider + { + private readonly string filename; + private readonly ILogger logger; + private bool synchronizedWithFile; + private string username; + private string password; + + public FileBasedCredentialsProvider(string filename, ILogger logger) + { + this.filename = filename; + this.logger = logger; + } + + private void ReadFile() + { + if (!System.IO.File.Exists(filename)) + { + return; + } + // storing the user/pass is generally a very bad idea as you could find the values by looking in the application memory. + // But since I highly doubt anyone is going to go that far to write a virus or app to read the memory of SubSync just to steal someones subtitle provider passwords. lol + try + { + var lines = System.IO.File.ReadAllLines(filename); + foreach (var line in lines) + { + var data = line.Split('='); + if (data[0].Equals("username", StringComparison.CurrentCultureIgnoreCase)) + { + this.username = data[1]; + } + else if (data[0].Equals("password", StringComparison.CurrentCultureIgnoreCase)) + { + this.password = data[1]; + } + } + synchronizedWithFile = true; + } + catch (Exception exc) + { + this.logger.WriteLine("@yel@Unable to read opensubtitles.auth! username and password will be left blank!"); + } + + } + + public AuthCredentials Get() + { + if (!this.synchronizedWithFile) + { + // in case it previously failed or file was added after the application started. + this.ReadFile(); + } + + return new AuthCredentials(this.username, this.password); + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/IAuthCredentialProvider.cs b/src/SubSync/Core/IAuthCredentialProvider.cs new file mode 100644 index 0000000..ad58cbf --- /dev/null +++ b/src/SubSync/Core/IAuthCredentialProvider.cs @@ -0,0 +1,7 @@ +namespace SubSync +{ + internal interface IAuthCredentialProvider + { + AuthCredentials Get(); + } +} \ No newline at end of file diff --git a/src/SubSync/Core/IFileSystemWatcher.cs b/src/SubSync/Core/IFileSystemWatcher.cs index 1beb7f3..c81f248 100644 --- a/src/SubSync/Core/IFileSystemWatcher.cs +++ b/src/SubSync/Core/IFileSystemWatcher.cs @@ -2,7 +2,7 @@ namespace SubSync { - internal interface IFileSystemWatcher : IDisposable + internal interface IFileSystemWatcher { void Start(); void Stop(); diff --git a/src/SubSync/Core/IStatusReporter.cs b/src/SubSync/Core/IStatusReporter.cs new file mode 100644 index 0000000..d320f2a --- /dev/null +++ b/src/SubSync/Core/IStatusReporter.cs @@ -0,0 +1,20 @@ +using System; + +namespace SubSync +{ + + internal interface IStatusResultReporter : IStatusReporter + { + event EventHandler OnReportFinished; + } + + internal interface IStatusReporter : IStatusReporter + { + void Report(TReportData data); + } + + internal interface IStatusReporter + { + void FinishReport(); + } +} \ No newline at end of file diff --git a/src/SubSync/Core/ISubtitleProvider.cs b/src/SubSync/Core/ISubtitleProvider.cs index 87dd013..1dbbd85 100644 --- a/src/SubSync/Core/ISubtitleProvider.cs +++ b/src/SubSync/Core/ISubtitleProvider.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace SubSync { diff --git a/src/SubSync/Core/IWorkerProvider.cs b/src/SubSync/Core/IWorkerProvider.cs index 84c9db5..61f2634 100644 --- a/src/SubSync/Core/IWorkerProvider.cs +++ b/src/SubSync/Core/IWorkerProvider.cs @@ -2,6 +2,6 @@ { internal interface IWorkerProvider { - IWorker GetWorker(IWorkerQueue queue, string file); + IWorker GetWorker(IWorkerQueue queue, string file, int tryCount = 0); } } diff --git a/src/SubSync/Core/IWorkerQueue.cs b/src/SubSync/Core/IWorkerQueue.cs index 62bd88f..04e0c57 100644 --- a/src/SubSync/Core/IWorkerQueue.cs +++ b/src/SubSync/Core/IWorkerQueue.cs @@ -4,10 +4,9 @@ namespace SubSync { internal interface IWorkerQueue : IDisposable { - event EventHandler QueueCompleted; - void Enqueue(string fullFilePath); - void Enqueue(IWorker worker); + bool Enqueue(string fullFilePath); void Start(); void Stop(); + void Reset(); } } \ No newline at end of file diff --git a/src/SubSync/Core/QueueProcessReporter.cs b/src/SubSync/Core/QueueProcessReporter.cs new file mode 100644 index 0000000..af05c39 --- /dev/null +++ b/src/SubSync/Core/QueueProcessReporter.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace SubSync +{ + internal class QueueProcessReporter : IStatusReporter, IStatusResultReporter + { + private readonly List failed = new List(); + private readonly object reportMutex = new object(); + private int total = 0; + private int succeeded = 0; + private int changes = 0; + + public event EventHandler OnReportFinished; + + public void Report(WorkerStatus data) + { + lock (reportMutex) + { + ++total; + if (data.Succeeded) + { + ++succeeded; + } + else + { + failed.Add(data.Target); + } + } + Interlocked.Increment(ref changes); + } + + public void FinishReport() + { + var changeCount = Interlocked.Exchange(ref changes, 0); + if (changeCount == 0) + { + return; + } + + lock (reportMutex) + { + OnReportFinished?.Invoke(this, new QueueProcessResult(total, succeeded, failed.ToArray())); + total = 0; + succeeded = 0; + failed.Clear(); + } + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/QueueProcessResult.cs b/src/SubSync/Core/QueueProcessResult.cs new file mode 100644 index 0000000..2e75b78 --- /dev/null +++ b/src/SubSync/Core/QueueProcessResult.cs @@ -0,0 +1,16 @@ +namespace SubSync +{ + internal struct QueueProcessResult + { + public readonly int Total; + public readonly int Succeeded; + public readonly string[] Failed; + + public QueueProcessResult(int total, int succeeded, string[] failed) + { + Total = total; + Succeeded = succeeded; + Failed = failed; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/SubtitleLanguage.cs b/src/SubSync/Core/SubtitleLanguage.cs new file mode 100644 index 0000000..20b3685 --- /dev/null +++ b/src/SubSync/Core/SubtitleLanguage.cs @@ -0,0 +1,537 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SubSync +{ + internal class SubtitleLanguage + { + public string LanguageId { get; } + public string Iso639 { get; } + public string Name { get; } + + public SubtitleLanguage(string languageId, string iso639, string name) + { + LanguageId = languageId; + Iso639 = iso639; + Name = name; + } + + public static SubtitleLanguage Find(string nameIsoOrId) + { + var possibleMatches = new List(); + + foreach (var item in All) + { + if (item.LanguageId.Equals(nameIsoOrId, StringComparison.OrdinalIgnoreCase)) + { + return item; + } + + if (!string.IsNullOrEmpty(item.Iso639) && item.Iso639.Equals(nameIsoOrId, StringComparison.OrdinalIgnoreCase)) + { + return item; + } + + if (item.Name.IndexOf(nameIsoOrId, StringComparison.OrdinalIgnoreCase) >= 0) + { + if (item.Name.Equals(nameIsoOrId, StringComparison.OrdinalIgnoreCase)) + { + return item; + } + + possibleMatches.Add(item); + } + } + + if (possibleMatches.Count == 1) + { + return possibleMatches[0]; + } + + return possibleMatches + .OrderByDescending(x => x.LanguageId) + .FirstOrDefault(); + } + + public static SubtitleLanguage[] All = { + new SubtitleLanguage("aar","aa","Afar, afar"), + new SubtitleLanguage("abk","ab","Abkhazian"), + new SubtitleLanguage("ace","","Achinese"), + new SubtitleLanguage("ach","","Acoli"), + new SubtitleLanguage("ada","","Adangme"), + new SubtitleLanguage("ady","","adyghé"), + new SubtitleLanguage("afa","","Afro-Asiatic (Other)"), + new SubtitleLanguage("afh","","Afrihili"), + new SubtitleLanguage("afr","af","Afrikaans"), + new SubtitleLanguage("ain","","Ainu"), + new SubtitleLanguage("aka","ak","Akan"), + new SubtitleLanguage("akk","","Akkadian"), + new SubtitleLanguage("alb","sq","Albanian"), + new SubtitleLanguage("ale","","Aleut"), + new SubtitleLanguage("alg","","Algonquian languages"), + new SubtitleLanguage("alt","","Southern Altai"), + new SubtitleLanguage("amh","am","Amharic"), + new SubtitleLanguage("ang","","English, Old (ca.450-1100)"), + new SubtitleLanguage("apa","","Apache languages"), + new SubtitleLanguage("ara","ar","Arabic"), + new SubtitleLanguage("arc","","Aramaic"), + new SubtitleLanguage("arg","an","Aragonese"), + new SubtitleLanguage("arm","hy","Armenian"), + new SubtitleLanguage("arn","","Araucanian"), + new SubtitleLanguage("arp","","Arapaho"), + new SubtitleLanguage("art","","Artificial (Other)"), + new SubtitleLanguage("arw","","Arawak"), + new SubtitleLanguage("asm","as","Assamese"), + new SubtitleLanguage("ast","at","Asturian"), + new SubtitleLanguage("ath","","Athapascan languages"), + new SubtitleLanguage("aus","","Australian languages"), + new SubtitleLanguage("ava","av","Avaric"), + new SubtitleLanguage("ave","ae","Avestan"), + new SubtitleLanguage("awa","","Awadhi"), + new SubtitleLanguage("aym","ay","Aymara"), + new SubtitleLanguage("aze","az","Azerbaijani"), + new SubtitleLanguage("bad","","Banda"), + new SubtitleLanguage("bai","","Bamileke languages"), + new SubtitleLanguage("bak","ba","Bashkir"), + new SubtitleLanguage("bal","","Baluchi"), + new SubtitleLanguage("bam","bm","Bambara"), + new SubtitleLanguage("ban","","Balinese"), + new SubtitleLanguage("baq","eu","Basque"), + new SubtitleLanguage("bas","","Basa"), + new SubtitleLanguage("bat","","Baltic (Other)"), + new SubtitleLanguage("bej","","Beja"), + new SubtitleLanguage("bel","be","Belarusian"), + new SubtitleLanguage("bem","","Bemba"), + new SubtitleLanguage("ben","bn","Bengali"), + new SubtitleLanguage("ber","","Berber (Other)"), + new SubtitleLanguage("bho","","Bhojpuri"), + new SubtitleLanguage("bih","bh","Bihari"), + new SubtitleLanguage("bik","","Bikol"), + new SubtitleLanguage("bin","","Bini"), + new SubtitleLanguage("bis","bi","Bislama"), + new SubtitleLanguage("bla","","Siksika"), + new SubtitleLanguage("bnt","","Bantu (Other)"), + new SubtitleLanguage("bos","bs","Bosnian"), + new SubtitleLanguage("bra","","Braj"), + new SubtitleLanguage("bre","br","Breton"), + new SubtitleLanguage("btk","","Batak (Indonesia)"), + new SubtitleLanguage("bua","","Buriat"), + new SubtitleLanguage("bug","","Buginese"), + new SubtitleLanguage("bul","bg","Bulgarian"), + new SubtitleLanguage("bur","my","Burmese"), + new SubtitleLanguage("byn","","Blin"), + new SubtitleLanguage("cad","","Caddo"), + new SubtitleLanguage("cai","","Central American Indian (Other)"), + new SubtitleLanguage("car","","Carib"), + new SubtitleLanguage("cat","ca","Catalan"), + new SubtitleLanguage("cau","","Caucasian (Other)"), + new SubtitleLanguage("ceb","","Cebuano"), + new SubtitleLanguage("cel","","Celtic (Other)"), + new SubtitleLanguage("cha","ch","Chamorro"), + new SubtitleLanguage("chb","","Chibcha"), + new SubtitleLanguage("che","ce","Chechen"), + new SubtitleLanguage("chg","","Chagatai"), + new SubtitleLanguage("chi","zh","Chinese (simplified)"), + new SubtitleLanguage("chk","","Chuukese"), + new SubtitleLanguage("chm","","Mari"), + new SubtitleLanguage("chn","","Chinook jargon"), + new SubtitleLanguage("cho","","Choctaw"), + new SubtitleLanguage("chp","","Chipewyan"), + new SubtitleLanguage("chr","","Cherokee"), + new SubtitleLanguage("chu","cu","Church Slavic"), + new SubtitleLanguage("chv","cv","Chuvash"), + new SubtitleLanguage("chy","","Cheyenne"), + new SubtitleLanguage("cmc","","Chamic languages"), + new SubtitleLanguage("cop","","Coptic"), + new SubtitleLanguage("cor","kw","Cornish"), + new SubtitleLanguage("cos","co","Corsican"), + new SubtitleLanguage("cpe","","Creoles and pidgins, English based (Other)"), + new SubtitleLanguage("cpf","","Creoles and pidgins, French-based (Other)"), + new SubtitleLanguage("cpp","","Creoles and pidgins, Portuguese-based (Other)"), + new SubtitleLanguage("cre","cr","Cree"), + new SubtitleLanguage("crh","","Crimean Tatar"), + new SubtitleLanguage("crp","","Creoles and pidgins (Other)"), + new SubtitleLanguage("csb","","Kashubian"), + new SubtitleLanguage("cus","","Cushitic (Other)' couchitiques, autres langues"), + new SubtitleLanguage("cze","cs","Czech"), + new SubtitleLanguage("dak","","Dakota"), + new SubtitleLanguage("dan","da","Danish"), + new SubtitleLanguage("dar","","Dargwa"), + new SubtitleLanguage("day","","Dayak"), + new SubtitleLanguage("del","","Delaware"), + new SubtitleLanguage("den","","Slave (Athapascan)"), + new SubtitleLanguage("dgr","","Dogrib"), + new SubtitleLanguage("din","","Dinka"), + new SubtitleLanguage("div","dv","Divehi"), + new SubtitleLanguage("doi","","Dogri"), + new SubtitleLanguage("dra","","Dravidian (Other)"), + new SubtitleLanguage("dua","","Duala"), + new SubtitleLanguage("dum","","Dutch, Middle (ca.1050-1350)"), + new SubtitleLanguage("dut","nl","Dutch"), + new SubtitleLanguage("dyu","","Dyula"), + new SubtitleLanguage("dzo","dz","Dzongkha"), + new SubtitleLanguage("efi","","Efik"), + new SubtitleLanguage("egy","","Egyptian (Ancient)"), + new SubtitleLanguage("eka","","Ekajuk"), + new SubtitleLanguage("elx","","Elamite"), + new SubtitleLanguage("eng","en","English"), + new SubtitleLanguage("enm","","English, Middle (1100-1500)"), + new SubtitleLanguage("epo","eo","Esperanto"), + new SubtitleLanguage("est","et","Estonian"), + new SubtitleLanguage("ewe","ee","Ewe"), + new SubtitleLanguage("ewo","","Ewondo"), + new SubtitleLanguage("fan","","Fang"), + new SubtitleLanguage("fao","fo","Faroese"), + new SubtitleLanguage("fat","","Fanti"), + new SubtitleLanguage("fij","fj","Fijian"), + new SubtitleLanguage("fil","","Filipino"), + new SubtitleLanguage("fin","fi","Finnish"), + new SubtitleLanguage("fiu","","Finno-Ugrian (Other)"), + new SubtitleLanguage("fon","","Fon"), + new SubtitleLanguage("fre","fr","French"), + new SubtitleLanguage("frm","","French, Middle (ca.1400-1600)"), + new SubtitleLanguage("fro","","French, Old (842-ca.1400)"), + new SubtitleLanguage("fry","fy","Frisian"), + new SubtitleLanguage("ful","ff","Fulah"), + new SubtitleLanguage("fur","","Friulian"), + new SubtitleLanguage("gaa","","Ga"), + new SubtitleLanguage("gay","","Gayo"), + new SubtitleLanguage("gba","","Gbaya"), + new SubtitleLanguage("gem","","Germanic (Other)"), + new SubtitleLanguage("geo","ka","Georgian"), + new SubtitleLanguage("ger","de","German"), + new SubtitleLanguage("gez","","Geez"), + new SubtitleLanguage("gil","","Gilbertese"), + new SubtitleLanguage("gla","gd","Gaelic"), + new SubtitleLanguage("gle","ga","Irish"), + new SubtitleLanguage("glg","gl","Galician"), + new SubtitleLanguage("glv","gv","Manx"), + new SubtitleLanguage("gmh","","German, Middle High (ca.1050-1500)"), + new SubtitleLanguage("goh","","German, Old High (ca.750-1050)"), + new SubtitleLanguage("gon","","Gondi"), + new SubtitleLanguage("gor","","Gorontalo"), + new SubtitleLanguage("got","","Gothic"), + new SubtitleLanguage("grb","","Grebo"), + new SubtitleLanguage("grc","","Greek, Ancient (to 1453)"), + new SubtitleLanguage("ell","el","Greek"), + new SubtitleLanguage("grn","gn","Guarani"), + new SubtitleLanguage("guj","gu","Gujarati"), + new SubtitleLanguage("gwi","","Gwich´in"), + new SubtitleLanguage("hai","","Haida"), + new SubtitleLanguage("hat","ht","Haitian"), + new SubtitleLanguage("hau","ha","Hausa"), + new SubtitleLanguage("haw","","Hawaiian"), + new SubtitleLanguage("heb","he","Hebrew"), + new SubtitleLanguage("her","hz","Herero"), + new SubtitleLanguage("hil","","Hiligaynon"), + new SubtitleLanguage("him","","Himachali"), + new SubtitleLanguage("hin","hi","Hindi"), + new SubtitleLanguage("hit","","Hittite"), + new SubtitleLanguage("hmn","","Hmong"), + new SubtitleLanguage("hmo","ho","Hiri Motu"), + new SubtitleLanguage("hrv","hr","Croatian"), + new SubtitleLanguage("hun","hu","Hungarian"), + new SubtitleLanguage("hup","","Hupa"), + new SubtitleLanguage("iba","","Iban"), + new SubtitleLanguage("ibo","ig","Igbo"), + new SubtitleLanguage("ice","is","Icelandic"), + new SubtitleLanguage("ido","io","Ido"), + new SubtitleLanguage("iii","ii","Sichuan Yi"), + new SubtitleLanguage("ijo","","Ijo"), + new SubtitleLanguage("iku","iu","Inuktitut"), + new SubtitleLanguage("ile","ie","Interlingue"), + new SubtitleLanguage("ilo","","Iloko"), + new SubtitleLanguage("ina","ia","Interlingua (International Auxiliary Language Asso"), + new SubtitleLanguage("inc","","Indic (Other)"), + new SubtitleLanguage("ind","id","Indonesian"), + new SubtitleLanguage("ine","","Indo-European (Other)"), + new SubtitleLanguage("inh","","Ingush"), + new SubtitleLanguage("ipk","ik","Inupiaq"), + new SubtitleLanguage("ira","","Iranian (Other)"), + new SubtitleLanguage("iro","","Iroquoian languages"), + new SubtitleLanguage("ita","it","Italian"), + new SubtitleLanguage("jav","jv","Javanese"), + new SubtitleLanguage("jpn","ja","Japanese"), + new SubtitleLanguage("jpr","","Judeo-Persian"), + new SubtitleLanguage("jrb","","Judeo-Arabic"), + new SubtitleLanguage("kaa","","Kara-Kalpak"), + new SubtitleLanguage("kab","","Kabyle"), + new SubtitleLanguage("kac","","Kachin"), + new SubtitleLanguage("kal","kl","Kalaallisut"), + new SubtitleLanguage("kam","","Kamba"), + new SubtitleLanguage("kan","kn","Kannada"), + new SubtitleLanguage("kar","","Karen"), + new SubtitleLanguage("kas","ks","Kashmiri"), + new SubtitleLanguage("kau","kr","Kanuri"), + new SubtitleLanguage("kaw","","Kawi"), + new SubtitleLanguage("kaz","kk","Kazakh"), + new SubtitleLanguage("kbd","","Kabardian"), + new SubtitleLanguage("kha","","Khasi"), + new SubtitleLanguage("khi","","Khoisan (Other)"), + new SubtitleLanguage("khm","km","Khmer"), + new SubtitleLanguage("kho","","Khotanese"), + new SubtitleLanguage("kik","ki","Kikuyu"), + new SubtitleLanguage("kin","rw","Kinyarwanda"), + new SubtitleLanguage("kir","ky","Kirghiz"), + new SubtitleLanguage("kmb","","Kimbundu"), + new SubtitleLanguage("kok","","Konkani"), + new SubtitleLanguage("kom","kv","Komi"), + new SubtitleLanguage("kon","kg","Kongo"), + new SubtitleLanguage("kor","ko","Korean"), + new SubtitleLanguage("kos","","Kosraean"), + new SubtitleLanguage("kpe","","Kpelle"), + new SubtitleLanguage("krc","","Karachay-Balkar"), + new SubtitleLanguage("kro","","Kru"), + new SubtitleLanguage("kru","","Kurukh"), + new SubtitleLanguage("kua","kj","Kuanyama"), + new SubtitleLanguage("kum","","Kumyk"), + new SubtitleLanguage("kur","ku","Kurdish"), + new SubtitleLanguage("kut","","Kutenai"), + new SubtitleLanguage("lad","","Ladino"), + new SubtitleLanguage("lah","","Lahnda"), + new SubtitleLanguage("lam","","Lamba"), + new SubtitleLanguage("lao","lo","Lao"), + new SubtitleLanguage("lat","la","Latin"), + new SubtitleLanguage("lav","lv","Latvian"), + new SubtitleLanguage("lez","","Lezghian"), + new SubtitleLanguage("lim","li","Limburgan"), + new SubtitleLanguage("lin","ln","Lingala"), + new SubtitleLanguage("lit","lt","Lithuanian"), + new SubtitleLanguage("lol","","Mongo"), + new SubtitleLanguage("loz","","Lozi"), + new SubtitleLanguage("ltz","lb","Luxembourgish"), + new SubtitleLanguage("lua","","Luba-Lulua"), + new SubtitleLanguage("lub","lu","Luba-Katanga"), + new SubtitleLanguage("lug","lg","Ganda"), + new SubtitleLanguage("lui","","Luiseno"), + new SubtitleLanguage("lun","","Lunda"), + new SubtitleLanguage("luo","","Luo (Kenya and Tanzania)"), + new SubtitleLanguage("lus","","lushai"), + new SubtitleLanguage("mac","mk","Macedonian"), + new SubtitleLanguage("mad","","Madurese"), + new SubtitleLanguage("mag","","Magahi"), + new SubtitleLanguage("mah","mh","Marshallese"), + new SubtitleLanguage("mai","","Maithili"), + new SubtitleLanguage("mak","","Makasar"), + new SubtitleLanguage("mal","ml","Malayalam"), + new SubtitleLanguage("man","","Mandingo"), + new SubtitleLanguage("mao","mi","Maori"), + new SubtitleLanguage("map","","Austronesian (Other)"), + new SubtitleLanguage("mar","mr","Marathi"), + new SubtitleLanguage("mas","","Masai"), + new SubtitleLanguage("may","ms","Malay"), + new SubtitleLanguage("mdf","","Moksha"), + new SubtitleLanguage("mdr","","Mandar"), + new SubtitleLanguage("men","","Mende"), + new SubtitleLanguage("mga","","Irish, Middle (900-1200)"), + new SubtitleLanguage("mic","","Mi'kmaq"), + new SubtitleLanguage("min","","Minangkabau"), + new SubtitleLanguage("mis","","Miscellaneous languages"), + new SubtitleLanguage("mkh","","Mon-Khmer (Other)"), + new SubtitleLanguage("mlg","mg","Malagasy"), + new SubtitleLanguage("mlt","mt","Maltese"), + new SubtitleLanguage("mnc","","Manchu"), + new SubtitleLanguage("mni","ma","Manipuri"), + new SubtitleLanguage("mno","","Manobo languages"), + new SubtitleLanguage("moh","","Mohawk"), + new SubtitleLanguage("mol","mo","Moldavian"), + new SubtitleLanguage("mon","mn","Mongolian"), + new SubtitleLanguage("mos","","Mossi"), + new SubtitleLanguage("mwl","","Mirandese"), + new SubtitleLanguage("mul","","Multiple languages"), + new SubtitleLanguage("mun","","Munda languages"), + new SubtitleLanguage("mus","","Creek"), + new SubtitleLanguage("mwr","","Marwari"), + new SubtitleLanguage("myn","","Mayan languages"), + new SubtitleLanguage("myv","","Erzya"), + new SubtitleLanguage("nah","","Nahuatl"), + new SubtitleLanguage("nai","","North American Indian"), + new SubtitleLanguage("nap","","Neapolitan"), + new SubtitleLanguage("nau","na","Nauru"), + new SubtitleLanguage("nav","nv","Navajo"), + new SubtitleLanguage("nbl","nr","Ndebele, South"), + new SubtitleLanguage("nde","nd","Ndebele, North"), + new SubtitleLanguage("ndo","ng","Ndonga"), + new SubtitleLanguage("nds","","Low German"), + new SubtitleLanguage("nep","ne","Nepali"), + new SubtitleLanguage("new","","Nepal Bhasa"), + new SubtitleLanguage("nia","","Nias"), + new SubtitleLanguage("nic","","Niger-Kordofanian (Other)"), + new SubtitleLanguage("niu","","Niuean"), + new SubtitleLanguage("nno","nn","Norwegian Nynorsk"), + new SubtitleLanguage("nob","nb","Norwegian Bokmal"), + new SubtitleLanguage("nog","","Nogai"), + new SubtitleLanguage("non","","Norse, Old"), + new SubtitleLanguage("nor","no","Norwegian"), + new SubtitleLanguage("nso","","Northern Sotho"), + new SubtitleLanguage("nub","","Nubian languages"), + new SubtitleLanguage("nwc","","Classical Newari"), + new SubtitleLanguage("nya","ny","Chichewa"), + new SubtitleLanguage("nym","","Nyamwezi"), + new SubtitleLanguage("nyn","","Nyankole"), + new SubtitleLanguage("nyo","","Nyoro"), + new SubtitleLanguage("nzi","","Nzima"), + new SubtitleLanguage("oci","oc","Occitan"), + new SubtitleLanguage("oji","oj","Ojibwa"), + new SubtitleLanguage("ori","or","Oriya"), + new SubtitleLanguage("orm","om","Oromo"), + new SubtitleLanguage("osa","","Osage"), + new SubtitleLanguage("oss","os","Ossetian"), + new SubtitleLanguage("ota","","Turkish, Ottoman (1500-1928)"), + new SubtitleLanguage("oto","","Otomian languages"), + new SubtitleLanguage("paa","","Papuan (Other)"), + new SubtitleLanguage("pag","","Pangasinan"), + new SubtitleLanguage("pal","","Pahlavi"), + new SubtitleLanguage("pam","","Pampanga"), + new SubtitleLanguage("pan","pa","Panjabi"), + new SubtitleLanguage("pap","","Papiamento"), + new SubtitleLanguage("pau","","Palauan"), + new SubtitleLanguage("peo","","Persian, Old (ca.600-400 B.C.)"), + new SubtitleLanguage("per","fa","Persian"), + new SubtitleLanguage("phi","","Philippine (Other)"), + new SubtitleLanguage("phn","","Phoenician"), + new SubtitleLanguage("pli","pi","Pali"), + new SubtitleLanguage("pol","pl","Polish"), + new SubtitleLanguage("pon","","Pohnpeian"), + new SubtitleLanguage("por","pt","Portuguese"), + new SubtitleLanguage("pra","","Prakrit languages"), + new SubtitleLanguage("pro","","Provençal, Old (to 1500)"), + new SubtitleLanguage("pus","ps","Pushto"), + new SubtitleLanguage("que","qu","Quechua"), + new SubtitleLanguage("raj","","Rajasthani"), + new SubtitleLanguage("rap","","Rapanui"), + new SubtitleLanguage("rar","","Rarotongan"), + new SubtitleLanguage("roa","","Romance (Other)"), + new SubtitleLanguage("roh","rm","Raeto-Romance"), + new SubtitleLanguage("rom","","Romany"), + new SubtitleLanguage("run","rn","Rundi"), + new SubtitleLanguage("rup","","Aromanian"), + new SubtitleLanguage("rus","ru","Russian"), + new SubtitleLanguage("sad","","Sandawe"), + new SubtitleLanguage("sag","sg","Sango"), + new SubtitleLanguage("sah","","Yakut"), + new SubtitleLanguage("sai","","South American Indian (Other)"), + new SubtitleLanguage("sal","","Salishan languages"), + new SubtitleLanguage("sam","","Samaritan Aramaic"), + new SubtitleLanguage("san","sa","Sanskrit"), + new SubtitleLanguage("sas","","Sasak"), + new SubtitleLanguage("sat","","Santali"), + new SubtitleLanguage("scc","sr","Serbian"), + new SubtitleLanguage("scn","","Sicilian"), + new SubtitleLanguage("sco","","Scots"), + new SubtitleLanguage("sel","","Selkup"), + new SubtitleLanguage("sem","","Semitic (Other)"), + new SubtitleLanguage("sga","","Irish, Old (to 900)"), + new SubtitleLanguage("sgn","","Sign Languages"), + new SubtitleLanguage("shn","","Shan"), + new SubtitleLanguage("sid","","Sidamo"), + new SubtitleLanguage("sin","si","Sinhalese"), + new SubtitleLanguage("sio","","Siouan languages"), + new SubtitleLanguage("sit","","Sino-Tibetan (Other)"), + new SubtitleLanguage("sla","","Slavic (Other)"), + new SubtitleLanguage("slo","sk","Slovak"), + new SubtitleLanguage("slv","sl","Slovenian"), + new SubtitleLanguage("sma","","Southern Sami"), + new SubtitleLanguage("sme","se","Northern Sami"), + new SubtitleLanguage("smi","","Sami languages (Other)"), + new SubtitleLanguage("smj","","Lule Sami"), + new SubtitleLanguage("smn","","Inari Sami"), + new SubtitleLanguage("smo","sm","Samoan"), + new SubtitleLanguage("sms","","Skolt Sami"), + new SubtitleLanguage("sna","sn","Shona"), + new SubtitleLanguage("snd","sd","Sindhi"), + new SubtitleLanguage("snk","","Soninke"), + new SubtitleLanguage("sog","","Sogdian"), + new SubtitleLanguage("som","so","Somali"), + new SubtitleLanguage("son","","Songhai"), + new SubtitleLanguage("sot","st","Sotho, Southern"), + new SubtitleLanguage("spa","es","Spanish"), + new SubtitleLanguage("srd","sc","Sardinian"), + new SubtitleLanguage("srr","","Serer"), + new SubtitleLanguage("ssa","","Nilo-Saharan (Other)"), + new SubtitleLanguage("ssw","ss","Swati"), + new SubtitleLanguage("suk","","Sukuma"), + new SubtitleLanguage("sun","su","Sundanese"), + new SubtitleLanguage("sus","","Susu"), + new SubtitleLanguage("sux","","Sumerian"), + new SubtitleLanguage("swa","sw","Swahili"), + new SubtitleLanguage("swe","sv","Swedish"), + new SubtitleLanguage("syr","sy","Syriac"), + new SubtitleLanguage("tah","ty","Tahitian"), + new SubtitleLanguage("tai","","Tai (Other)"), + new SubtitleLanguage("tam","ta","Tamil"), + new SubtitleLanguage("tat","tt","Tatar"), + new SubtitleLanguage("tel","te","Telugu"), + new SubtitleLanguage("tem","","Timne"), + new SubtitleLanguage("ter","","Tereno"), + new SubtitleLanguage("tet","","Tetum"), + new SubtitleLanguage("tgk","tg","Tajik"), + new SubtitleLanguage("tgl","tl","Tagalog"), + new SubtitleLanguage("tha","th","Thai"), + new SubtitleLanguage("tib","bo","Tibetan"), + new SubtitleLanguage("tig","","Tigre"), + new SubtitleLanguage("tir","ti","Tigrinya"), + new SubtitleLanguage("tiv","","Tiv"), + new SubtitleLanguage("tkl","","Tokelau"), + new SubtitleLanguage("tlh","","Klingon"), + new SubtitleLanguage("tli","","Tlingit"), + new SubtitleLanguage("tmh","","Tamashek"), + new SubtitleLanguage("tog","","Tonga (Nyasa)"), + new SubtitleLanguage("ton","to","Tonga (Tonga Islands)"), + new SubtitleLanguage("tpi","","Tok Pisin"), + new SubtitleLanguage("tsi","","Tsimshian"), + new SubtitleLanguage("tsn","tn","Tswana"), + new SubtitleLanguage("tso","ts","Tsonga"), + new SubtitleLanguage("tuk","tk","Turkmen"), + new SubtitleLanguage("tum","","Tumbuka"), + new SubtitleLanguage("tup","","Tupi languages"), + new SubtitleLanguage("tur","tr","Turkish"), + new SubtitleLanguage("tut","","Altaic (Other)"), + new SubtitleLanguage("tvl","","Tuvalu"), + new SubtitleLanguage("twi","tw","Twi"), + new SubtitleLanguage("tyv","","Tuvinian"), + new SubtitleLanguage("udm","","Udmurt"), + new SubtitleLanguage("uga","","Ugaritic"), + new SubtitleLanguage("uig","ug","Uighur"), + new SubtitleLanguage("ukr","uk","Ukrainian"), + new SubtitleLanguage("umb","","Umbundu"), + new SubtitleLanguage("und","","Undetermined"), + new SubtitleLanguage("urd","ur","Urdu"), + new SubtitleLanguage("uzb","uz","Uzbek"), + new SubtitleLanguage("vai","","Vai"), + new SubtitleLanguage("ven","ve","Venda"), + new SubtitleLanguage("vie","vi","Vietnamese"), + new SubtitleLanguage("vol","vo","Volapük"), + new SubtitleLanguage("vot","","Votic"), + new SubtitleLanguage("wak","","Wakashan languages"), + new SubtitleLanguage("wal","","Walamo"), + new SubtitleLanguage("war","","Waray"), + new SubtitleLanguage("was","","Washo"), + new SubtitleLanguage("wel","cy","Welsh"), + new SubtitleLanguage("wen","","Sorbian languages"), + new SubtitleLanguage("wln","wa","Walloon"), + new SubtitleLanguage("wol","wo","Wolof"), + new SubtitleLanguage("xal","","Kalmyk"), + new SubtitleLanguage("xho","xh","Xhosa"), + new SubtitleLanguage("yao","","Yao"), + new SubtitleLanguage("yap","","Yapese"), + new SubtitleLanguage("yid","yi","Yiddish"), + new SubtitleLanguage("yor","yo","Yoruba"), + new SubtitleLanguage("ypk","","Yupik languages"), + new SubtitleLanguage("zap","","Zapotec"), + new SubtitleLanguage("zen","","Zenaga"), + new SubtitleLanguage("zha","za","Zhuang"), + new SubtitleLanguage("znd","","Zande"), + new SubtitleLanguage("zul","zu","Zulu"), + new SubtitleLanguage("zun","","Zuni"), + new SubtitleLanguage("rum","ro","Romanian"), + new SubtitleLanguage("pob","pb","Portuguese (BR)"), + new SubtitleLanguage("mne","me","Montenegrin"), + new SubtitleLanguage("zht","zt","Chinese (traditional)"), + new SubtitleLanguage("zhe","ze","Chinese bilingual"), + new SubtitleLanguage("pom","pm","Portuguese (MZ)"), + new SubtitleLanguage("ext","ex","Extremadura"), + }; + } +} \ No newline at end of file diff --git a/src/SubSync/Core/SubtitleProviderBase.cs b/src/SubSync/Core/SubtitleProviderBase.cs index 1db5007..218e53a 100644 --- a/src/SubSync/Core/SubtitleProviderBase.cs +++ b/src/SubSync/Core/SubtitleProviderBase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -10,15 +11,17 @@ namespace SubSync internal abstract class SubtitleProviderBase : ISubtitleProvider { protected readonly HashSet Languages; - + protected SubtitleProviderBase(HashSet languages) { this.Languages = languages; } - protected abstract int RequestRetryLimit { get; } + protected string UserAgent { get; set; } = "SubSync10"; + + protected int RequestRetryLimit { get; set; } = 3; - protected abstract int RequestTimeout { get; } + protected int RequestTimeout { get; set; } = 3000; public abstract Task GetAsync(string name, string outputDirectory); @@ -27,8 +30,7 @@ protected async Task DownloadFileAsync(string url, string outputDirector try { var filename = "download.zip"; - var req = HttpWebRequest.CreateHttp(url); - req.Timeout = req.ReadWriteTimeout = RequestTimeout; + var req = CreateRequest(url); using (var response = (HttpWebResponse)await req.GetResponseAsync()) { var contentDisposition = response.Headers.Get("Content-Disposition"); @@ -79,14 +81,7 @@ protected async Task DownloadStringAsync(string url, int retryCount = 0) { try { - var req = HttpWebRequest.CreateHttp(url); - req.Timeout = req.ReadWriteTimeout = RequestTimeout; - using (var response = await req.GetResponseAsync()) - using (var stream = response.GetResponseStream()) - using (var sr = new StreamReader(stream)) - { - return await sr.ReadToEndAsync(); - } + return await GetResponseStringAsync(CreateRequest(url)); } catch (WebException webException) { @@ -105,6 +100,81 @@ protected async Task DownloadStringAsync(string url, int retryCount = 0) } } + protected async Task GetResponseStringAsync(HttpWebRequest request) + { + using (var response = await request.GetResponseAsync()) + { + return await GetResponseStringAsync(response); + } + } + + protected async Task GetResponseStringAsync(WebResponse response) + { + using (var stream = response.GetResponseStream()) + using (var sr = new StreamReader(stream)) + { + return await sr.ReadToEndAsync(); + } + } + + protected HttpWebRequest CreatePostAsync(string url, string data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (data.StartsWith("<")) + { + return CreatePostXmlAsync(url, data); + } + + if (data.StartsWith("[") || data.StartsWith("{")) + { + return CreatePostJsonAsync(url, data); + } + + return CreatePostTextAsync(url, data); + } + + protected HttpWebRequest CreatePostXmlAsync(string url, string data) + { + return CreatePostRequest(url, data, "text/xml"); + } + + protected HttpWebRequest CreatePostJsonAsync(string url, string data) + { + return CreatePostRequest(url, data, "application/json"); + } + + protected HttpWebRequest CreatePostTextAsync(string url, string data) + { + return CreatePostRequest(url, data, "text/plain"); + } + + protected HttpWebRequest CreatePostRequest(string url, string data, string contentType) + { + var req = CreateRequest(url, "POST"); + req.ContentType = contentType; + req.Host = url.Split(new[] { "://" }, StringSplitOptions.None)[1].Split('/').FirstOrDefault(); + using (var reqStream = req.GetRequestStream()) + using (var writer = new StreamWriter(reqStream)) + { + writer.Write(data); + } + + return req; + } + + private HttpWebRequest CreateRequest(string url, string method = "GET") + { + var req = HttpWebRequest.CreateHttp(url); + req.Method = method; + req.UserAgent = UserAgent; + req.Timeout = req.ReadWriteTimeout = RequestTimeout; + return req; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static string GetUrlFriendlyName(string name) { diff --git a/src/SubSync/Core/SubtitleSynchronizer.cs b/src/SubSync/Core/SubtitleSynchronizer.cs index e0356ec..be186e7 100644 --- a/src/SubSync/Core/SubtitleSynchronizer.cs +++ b/src/SubSync/Core/SubtitleSynchronizer.cs @@ -7,10 +7,11 @@ namespace SubSync { - internal class SubtitleSynchronizer : IFileSystemWatcher + internal class SubtitleSynchronizer : IFileSystemWatcher, IDisposable { private readonly ILogger logger; private readonly IWorkerQueue workerQueue; + private readonly IStatusResultReporter statusReporter; private readonly string input; private readonly HashSet videoExtensions; private readonly HashSet subtitleExtensions; @@ -21,12 +22,14 @@ internal class SubtitleSynchronizer : IFileSystemWatcher public SubtitleSynchronizer( ILogger logger, IWorkerQueue workerQueue, + IStatusResultReporter statusReporter, string input, HashSet videoExtensions, HashSet subtitleExtensions) { this.logger = logger; this.workerQueue = workerQueue; + this.statusReporter = statusReporter; this.input = input; this.videoExtensions = videoExtensions; this.subtitleExtensions = subtitleExtensions; @@ -35,6 +38,7 @@ public SubtitleSynchronizer( public void Dispose() { if (this.disposed) return; + this.Stop(); this.fsWatcher?.Dispose(); this.workerQueue.Dispose(); disposed = true; @@ -53,7 +57,7 @@ public void Start() this.fsWatcher.Created += FileCreated; this.fsWatcher.Renamed += FileCreated; - this.workerQueue.QueueCompleted += ResultReport; + this.statusReporter.OnReportFinished += ResultReport; this.workerQueue.Start(); this.SyncAll(); } @@ -61,7 +65,7 @@ public void Start() public void Stop() { if (this.fsWatcher == null) return; - this.workerQueue.QueueCompleted -= ResultReport; + this.statusReporter.OnReportFinished -= ResultReport; this.workerQueue.Stop(); this.fsWatcher.Error -= FsWatcherOnError; this.fsWatcher.Created -= FileCreated; @@ -70,12 +74,12 @@ public void Stop() this.fsWatcher = null; } - private void ResultReport(object sender, QueueCompletedEventArgs e) + private void ResultReport(object sender, QueueProcessResult result) { var skipcount = Interlocked.Exchange(ref skipped, 0); - var total = e.Total; - var failed = e.Failed; - var success = e.Succeeded; + var total = result.Total; + var failed = result.Failed; + var success = result.Succeeded; if (total == 0 && skipcount == 0) { @@ -87,20 +91,26 @@ private void ResultReport(object sender, QueueCompletedEventArgs e) this.logger.WriteLine($""); this.logger.WriteLine($" @whi@Synchronization completed with a total of @yel@{total} @whi@video(s) processed."); + this.logger.WriteLine($" {skipcount} video(s) was skipped."); if (success > 0) { this.logger.WriteLine($" @green@{success} @whi@video(s) was successefully was synchronized."); } - if (failed > 0) + if (failed.Length > 0) { - this.logger.WriteLine($" @red@{failed} @whi@video(s) failed to synchronize."); + this.logger.WriteLine($" @red@{failed.Length} @whi@video(s) failed to synchronize."); + foreach (var failedItem in failed) + { + this.logger.WriteLine($" @red@* {System.IO.Path.GetFileName(failedItem)}"); + } } - this.logger.WriteLine($" {skipcount} video(s) was skipped."); + } public void SyncAll() { var directoryInfo = new DirectoryInfo(this.input); + this.workerQueue.Reset(); this.videoExtensions.SelectMany(y => directoryInfo.GetFiles($"*{y}", SearchOption.AllDirectories)).Select(x => x.FullName) .ForEach(Sync); } @@ -142,6 +152,10 @@ private void FileCreated(object sender, FileSystemEventArgs e) private bool IsSynchronized(string filePath) { var fileInfo = new FileInfo(filePath); + if (BlacklistedFile(fileInfo.Name)) + { + return true; + } var directory = fileInfo.Directory; if (directory == null) { @@ -153,5 +167,11 @@ private bool IsSynchronized(string filePath) directory.GetFiles($"{Path.GetFileNameWithoutExtension(fileInfo.Name)}{x}", SearchOption.AllDirectories)) .Any(); } + + private bool BlacklistedFile(string filename) + { + // todo: make this configurable. but for now, ignore all sample. files. + return Path.GetFileNameWithoutExtension(filename).Equals("sample", StringComparison.OrdinalIgnoreCase); + } } } \ No newline at end of file diff --git a/src/SubSync/Core/Worker.cs b/src/SubSync/Core/Worker.cs index a8d3ca3..8fa3ed2 100644 --- a/src/SubSync/Core/Worker.cs +++ b/src/SubSync/Core/Worker.cs @@ -11,92 +11,97 @@ namespace SubSync { internal class Worker : IWorker { - private const int RetryLimit = 5; private readonly string filePath; private readonly ILogger logger; private readonly IWorkerQueue workerQueue; private readonly ISubtitleProvider subtitleProvider; + private readonly IStatusReporter statusReporter; private readonly HashSet subtitleExtensions; + private readonly int retryCount; + private static readonly HashSet FileCompressionExtensions = new HashSet { ".zip", ".rar", ".gzip", ".gz", ".7z", ".tar", ".tar.gz" }; - private TaskCompletionSource taskCompletionSource; - - private int syncCount; + private TaskCompletionSource taskCompletionSource = null; public Worker( string filePath, ILogger logger, IWorkerQueue workerQueue, ISubtitleProvider subtitleProvider, - HashSet subtitleExtensions) + IStatusReporter statusReporter, + HashSet subtitleExtensions, + int retryCount = 0) { this.filePath = filePath; this.logger = logger; this.workerQueue = workerQueue; this.subtitleProvider = subtitleProvider; + this.statusReporter = statusReporter; this.subtitleExtensions = subtitleExtensions; + this.retryCount = retryCount; } public Task SyncAsync() { - if (taskCompletionSource == null - || taskCompletionSource.Task.Status == TaskStatus.RanToCompletion - || taskCompletionSource.Task.Status == TaskStatus.Canceled - || taskCompletionSource.Task.Status == TaskStatus.Faulted) + if (taskCompletionSource != null) { - taskCompletionSource = new TaskCompletionSource(); + return taskCompletionSource.Task; } + taskCompletionSource = new TaskCompletionSource(); + try { return taskCompletionSource.Task; } finally { - if (Volatile.Read(ref this.syncCount) == 0) + Task.Factory.StartNew(async () => { - Task.Factory.StartNew(async () => + if (this.retryCount > 0) { - var counter = Interlocked.Increment(ref this.syncCount); - var file = new FileInfo(filePath); - this.logger.WriteLine($"Synchronizing {file.Name}"); - try + await Task.Delay(this.retryCount * 1000); + } + + var file = new FileInfo(filePath); + this.logger.WriteLine($"Synchronizing {file.Name}"); + try + { + var directory = file.Directory?.FullName ?? "./"; + var outputName = await subtitleProvider.GetAsync(file.Name, directory); + var extension = Path.GetExtension(outputName); + if (IsCompressed(extension)) { - var directory = file.Directory?.FullName ?? "./"; - var outputName = await subtitleProvider.GetAsync(file.Name, directory); - var extension = Path.GetExtension(outputName); - if (IsCompressed(extension)) - { - outputName = await DecompressAsync(outputName); - } - - var finalName = Rename(outputName, Path.GetFileNameWithoutExtension(file.Name)); - this.logger.WriteLine( - $"@gray@Subtitle @white@{Path.GetFileName(finalName)} @green@downloaded!"); - Interlocked.Decrement(ref this.syncCount); - this.taskCompletionSource.SetResult(true); + outputName = await DecompressAsync(outputName); } - catch (NestedArchiveNotSupportedException nexc) + + var finalName = Rename(outputName, Path.GetFileNameWithoutExtension(file.Name)); + this.logger.WriteLine( + $"@gray@Subtitle @white@{Path.GetFileName(finalName)} @green@downloaded!"); + this.statusReporter.Report(new WorkerStatus(true, file.Name)); + this.taskCompletionSource.SetResult(true); + } + catch (NestedArchiveNotSupportedException nexc) + { + this.logger.Error($"Synchronization of {file.Name} failed with: {nexc.Message}"); + this.statusReporter.Report(new WorkerStatus(false, file.Name)); + this.taskCompletionSource.SetException(nexc); + } + catch (Exception exc) + { + this.logger.Error($"Synchronization of {file.Name} failed with: {exc.Message}"); + if (!this.workerQueue.Enqueue(filePath)) // (this); { - this.logger.Error($"Synchronization of {file.Name} failed with: {nexc.Message}"); - this.taskCompletionSource.SetException(nexc); + this.statusReporter.Report(new WorkerStatus(false, file.Name)); } - catch (Exception exc) - { - this.logger.Error($"Synchronization of {file.Name} failed with: {exc.Message}"); + this.taskCompletionSource.SetException(exc); + } - if (counter <= RetryLimit) - { - this.workerQueue.Enqueue(filePath); // (this); - } - this.taskCompletionSource.SetException(exc); - } + }, TaskCreationOptions.LongRunning); - }, TaskCreationOptions.LongRunning); - } } } @@ -155,7 +160,7 @@ private async Task DecompressArchive(string filename, Func IsCompressed(System.IO.Path.GetExtension(x.Key))); if (archive != null) { - logger.WriteLine($"@yel@Warning: Nested archive found inside '{filename}', output subtitle may not be correct!"); + logger.WriteLine($"@yel@Warning: Nested archive found inside '{filename}', output subtitle may not be correct!"); var result = await UnpackEntryAsync(archive, directory); targetFile = result.Filename; return await DecompressAsync(result.Filename); @@ -242,12 +247,4 @@ public EntryUnpackResult(bool subtitleFound, string filename, string entry) } } } - - internal class NestedArchiveNotSupportedException : Exception - { - public NestedArchiveNotSupportedException(string filename) - : base($"Downloaded archive, '{filename}' @red@contain another archive within it and cannot properly be extracted. Archive kept for manual labor.") - { - } - } } \ No newline at end of file diff --git a/src/SubSync/Core/WorkerProvider.cs b/src/SubSync/Core/WorkerProvider.cs index 943d038..a198cc9 100644 --- a/src/SubSync/Core/WorkerProvider.cs +++ b/src/SubSync/Core/WorkerProvider.cs @@ -6,26 +6,31 @@ internal class WorkerProvider : IWorkerProvider { private readonly ILogger logger; private readonly ISubtitleProvider subtitleProvider; + private readonly IStatusReporter statusReporter; private readonly HashSet subtitleExtensions; public WorkerProvider( ILogger logger, HashSet subtitleExtensions, - ISubtitleProvider subtitleProvider) + ISubtitleProvider subtitleProvider, + IStatusReporter statusReporter) { this.logger = logger; this.subtitleProvider = subtitleProvider; + this.statusReporter = statusReporter; this.subtitleExtensions = subtitleExtensions; } - public IWorker GetWorker(IWorkerQueue queue, string file) + public IWorker GetWorker(IWorkerQueue queue, string file, int tryCount = 0) { return new Worker( file, - this.logger, + logger, queue, - this.subtitleProvider, - subtitleExtensions); + subtitleProvider, + statusReporter, + subtitleExtensions, + tryCount); } } } \ No newline at end of file diff --git a/src/SubSync/Core/WorkerQueue.cs b/src/SubSync/Core/WorkerQueue.cs index fd275a3..8a10f08 100644 --- a/src/SubSync/Core/WorkerQueue.cs +++ b/src/SubSync/Core/WorkerQueue.cs @@ -9,18 +9,22 @@ namespace SubSync { internal class WorkerQueue : IWorkerQueue { - public event EventHandler QueueCompleted; private const int ConcurrentWorkers = 7; private readonly IWorkerProvider workerProvider; + private readonly IStatusReporter statusReporter; private readonly ConcurrentQueue queue = new ConcurrentQueue(); + private readonly ConcurrentDictionary queueTries = new ConcurrentDictionary(); private readonly Thread workerThread; private bool enabled; private bool disposed; - private int queueSize = 0; - public WorkerQueue(IWorkerProvider workerProvider) + // the max times the same item can be enqueued. + private const int RetryLimit = 3; + + public WorkerQueue(IWorkerProvider workerProvider, IStatusReporter statusReporter) { this.workerProvider = workerProvider; + this.statusReporter = statusReporter; this.workerThread = new Thread(ProcessQueue); } @@ -31,15 +35,17 @@ public void Dispose() this.disposed = true; } - public void Enqueue(string fullFilePath) + public bool Enqueue(string fullFilePath) { - this.Enqueue(this.workerProvider.GetWorker(this, fullFilePath)); - } + queueTries.TryGetValue(fullFilePath, out var tries); + if (tries < RetryLimit) + { + queueTries[fullFilePath] = tries + 1; + queue.Enqueue(this.workerProvider.GetWorker(this, fullFilePath, tries)); + return true; + } - public void Enqueue(IWorker worker) - { - Interlocked.Increment(ref queueSize); - queue.Enqueue(worker); + return false; } public void Start() @@ -56,12 +62,15 @@ public void Stop() workerThread.Join(); } + public void Reset() + { + queue.Clear(); + queueTries.Clear(); + } + private async void ProcessQueue() { var activeJobs = new List(); - var resultReported = false; - var failed = 0; - var succeeded = 0; do { while (activeJobs.Count < ConcurrentWorkers && this.queue.TryDequeue(out var worker)) @@ -71,24 +80,13 @@ private async void ProcessQueue() if (activeJobs.Count > 0) { - resultReported = false; await Task.WhenAny(activeJobs); - failed += activeJobs.Count(x => x.IsFaulted && x.IsCompleted); - succeeded += activeJobs.Count(x => !x.IsFaulted && x.IsCompleted); activeJobs = activeJobs.Where(x => !x.IsCompleted).ToList(); } else { - if (!resultReported && QueueCompleted != null) - { - QueueCompleted?.Invoke(this, new QueueCompletedEventArgs(Volatile.Read(ref queueSize), succeeded, failed)); - resultReported = true; - } - - succeeded = 0; - failed = 0; - Volatile.Write(ref queueSize, 0); - System.Threading.Thread.Sleep(100); + statusReporter.FinishReport();// should only report if it has been set dirty. This happens only if someone has reported data. + Thread.Sleep(100); } } while (this.enabled); diff --git a/src/SubSync/Core/WorkerStatus.cs b/src/SubSync/Core/WorkerStatus.cs new file mode 100644 index 0000000..e3fbaaf --- /dev/null +++ b/src/SubSync/Core/WorkerStatus.cs @@ -0,0 +1,14 @@ +namespace SubSync +{ + internal struct WorkerStatus + { + public readonly bool Succeeded; + public readonly string Target; + + public WorkerStatus(bool succeeded, string target) + { + Succeeded = succeeded; + Target = target; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/IXmlRpcObjectValue.cs b/src/SubSync/Core/XmlRpc/IXmlRpcObjectValue.cs new file mode 100644 index 0000000..240bf45 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/IXmlRpcObjectValue.cs @@ -0,0 +1,7 @@ +namespace SubSync +{ + public interface IXmlRpcObjectValue + { + object GetValue(); + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcArray.cs b/src/SubSync/Core/XmlRpc/XmlRpcArray.cs new file mode 100644 index 0000000..97e0055 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcArray.cs @@ -0,0 +1,25 @@ +namespace SubSync +{ + public class XmlRpcArray : XmlRpcObject + { + public XmlRpcObject[] Items { get; } + + public XmlRpcArray(XmlRpcObject[] items) + { + Items = items; + } + + public override XmlRpcObject FindRecursive(string name) + { + foreach (var item in Items) + { + var found = item.FindRecursive(name); + if (found != null) + { + return found; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcDouble.cs b/src/SubSync/Core/XmlRpc/XmlRpcDouble.cs new file mode 100644 index 0000000..2d59302 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcDouble.cs @@ -0,0 +1,27 @@ +namespace SubSync +{ + public class XmlRpcDouble : XmlRpcValueObject + { + public double Value { get; } + + public XmlRpcDouble(double value) + { + Value = value; + } + + public override string ToString() + { + return Value.ToString(); + } + + public static implicit operator double(XmlRpcDouble val) + { + return val.Value; + } + + public override object GetValue() + { + return Value; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcInt.cs b/src/SubSync/Core/XmlRpc/XmlRpcInt.cs new file mode 100644 index 0000000..b5eadc9 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcInt.cs @@ -0,0 +1,27 @@ +namespace SubSync +{ + public class XmlRpcInt : XmlRpcValueObject + { + public int Value { get; } + + public XmlRpcInt(int value) + { + Value = value; + } + + public override string ToString() + { + return Value.ToString(); + } + + public static implicit operator int(XmlRpcInt val) + { + return val.Value; + } + + public override object GetValue() + { + return Value; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcMember.cs b/src/SubSync/Core/XmlRpc/XmlRpcMember.cs new file mode 100644 index 0000000..b54541d --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcMember.cs @@ -0,0 +1,29 @@ +namespace SubSync +{ + public class XmlRpcMember : XmlRpcObject + { + public XmlRpcString Name { get; } + public XmlRpcObject Value { get; } + + public XmlRpcMember(XmlRpcString name, XmlRpcObject value) + { + Name = name; + Value = value; + } + + public override string ToString() + { + return "'" + Name + "' => '" + Value + "'"; + } + + public override XmlRpcObject FindRecursive(string name) + { + if (name == this.Name.ToString()) + { + return this; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcObject.cs b/src/SubSync/Core/XmlRpc/XmlRpcObject.cs new file mode 100644 index 0000000..99d3336 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcObject.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace SubSync +{ + public abstract class XmlRpcObject + { + protected XmlRpcObject() + { + } + + public static XmlRpcRoot Parse(string data) + { + var doc = XDocument.Parse(data); + if (doc.Root == null) + { + throw new ArgumentException("Argument is not a valid xml document.", nameof(data)); + } + + if (doc.Root.Name == "methodResponse") + { + return ParseMethodResponse(doc.Root); + } + + return new XmlRpcRoot(); + } + + private static XmlRpcRoot ParseMethodResponse(XElement doc) + { + var values = doc.Element("params"); + if (values == null) + { + throw new Exception("Xmlrpc response is invalid, missing params element."); + } + + var resultObject = new XmlRpcRoot(); + var parameters = values.Elements("param"); + foreach (var param in parameters) + { + var result = ParseParam(param); + resultObject.Add(result); + } + + return resultObject; + } + + + public abstract XmlRpcObject FindRecursive(string name); + + private static XmlRpcObject ParseParam(XElement param) + { + return ParseValue(param.Element("value")); + } + + private static XmlRpcObject ParseValue(XElement value) + { + foreach (var elm in value.Elements()) + { + if (elm.Name == "struct") + { + return ParseStruct(elm); + } + if (elm.Name == "array") + { + return ParseArray(elm); + } + if (elm.Name == "string") + { + return ParseString(elm); + } + if (elm.Name == "double") + { + return ParseDouble(elm); + } + if (elm.Name == "int") + { + return ParseInt(elm); + } + } + + return ParseString(value); + } + + private static XmlRpcInt ParseInt(XElement elm) + { + int.TryParse(elm.Value, out var value); + return new XmlRpcInt(value); + } + + private static XmlRpcDouble ParseDouble(XElement elm) + { + double.TryParse(elm.Value, out var value); + return new XmlRpcDouble(value); + } + + private static XmlRpcObject ParseString(XElement elm) + { + return new XmlRpcString(elm.Value); + } + + private static XmlRpcArray ParseArray(XElement elm) + { + var items = new List(); + var dataElement = elm.Element("data"); + var elements = dataElement.Elements("value"); + foreach (var elmData in elements) + { + var item = ParseValue(elmData); + items.Add(item); + } + + return new XmlRpcArray(items.ToArray()); + } + + private static XmlRpcObject ParseStruct(XElement elm) + { + var foundMembers = new List(); + var members = elm.Elements("member"); + foreach (var member in members) + { + var name = ParseValue(member.Element("name")) as XmlRpcString; + var value = ParseValue(member.Element("value")); + foundMembers.Add(new XmlRpcMember(name, value)); + } + return new XmlRpcStruct(foundMembers); + } + + public T Deserialize(string data) + { + // aint gonna write a xml parser today + // 1. parse as xml or xdoc + // 2. get properties of type T + // 3. create instance of T + // 4. assign all properties with values matching same name parsed from data. + // + return default(T); + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcRoot.cs b/src/SubSync/Core/XmlRpc/XmlRpcRoot.cs new file mode 100644 index 0000000..fdf1b92 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcRoot.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace SubSync +{ + public class XmlRpcRoot : XmlRpcObject + { + internal readonly List children = new List(); + + internal void Add(XmlRpcObject child) + { + children.Add(child); + } + + public T GetValue(string name) + { + if (this.children.Count > 0 && !string.IsNullOrEmpty(name)) + { + foreach (var child in children) + { + var node = child.FindRecursive(name); + if (node is XmlRpcMember member) + { + node = member.Value; + } + if (node is IXmlRpcObjectValue valueNode) + { + return (T)valueNode.GetValue(); + } + } + } + return default(T); + } + + public override XmlRpcObject FindRecursive(string name) + { + foreach (var child in children) + { + var found = child.FindRecursive(name); + if (found != null) + { + return found; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcString.cs b/src/SubSync/Core/XmlRpc/XmlRpcString.cs new file mode 100644 index 0000000..2e4b70f --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcString.cs @@ -0,0 +1,27 @@ +namespace SubSync +{ + public class XmlRpcString : XmlRpcValueObject + { + public string Value { get; } + + public XmlRpcString(string value) + { + Value = value; + } + + public override string ToString() + { + return Value; + } + + public static implicit operator string(XmlRpcString val) + { + return val.Value; + } + + public override object GetValue() + { + return Value; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs b/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs new file mode 100644 index 0000000..6a48969 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace SubSync +{ + public class XmlRpcStruct : XmlRpcObject + { + public List Members { get; } + + public XmlRpcStruct(List members) + { + Members = members; + } + + public override XmlRpcObject FindRecursive(string name) + { + foreach (var item in Members) + { + var found = item.FindRecursive(name); + if (found != null) + { + return found; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs b/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs new file mode 100644 index 0000000..4d2d15e --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs @@ -0,0 +1,12 @@ +namespace SubSync +{ + public abstract class XmlRpcValueObject : XmlRpcObject, IXmlRpcObjectValue + { + public override XmlRpcObject FindRecursive(string name) + { + return null; + } + + public abstract object GetValue(); + } +} \ No newline at end of file diff --git a/src/SubSync/Program.cs b/src/SubSync/Program.cs index 5eee89f..ad630ff 100644 --- a/src/SubSync/Program.cs +++ b/src/SubSync/Program.cs @@ -10,7 +10,8 @@ class Program { static void Main(string[] args) { - var input = "./"; + //var input = "./"; + var input = "E:\\filmer"; var videoExtensions = ParseList("*.avi;*.mp4;*.mkv;*.mpeg;*.flv;*.webm"); var subtitleExtensions = ParseList("*.srt;*.txt;*.sub;*.idx;*.ssa;*.ass"); var languages = ParseList("english"); @@ -33,44 +34,41 @@ static void Main(string[] args) var version = GetVersion(); var logger = new ConsoleLogger(); - var fallbackSubtitleProvider = new FallbackSubtitleProvider( - //new OpenSubtitles(languages), // uncomment as soon as its been implemented - new Subscene(languages) - ); - - var subSyncWorkerProvider = new WorkerProvider(logger, subtitleExtensions, fallbackSubtitleProvider); - var subSyncWorkerQueue = new WorkerQueue(subSyncWorkerProvider); - - using (var mediaWatcher = new SubtitleSynchronizer(logger, subSyncWorkerQueue, input, videoExtensions, subtitleExtensions)) + using (var fallbackSubtitleProvider = new FallbackSubtitleProvider( + new OpenSubtitles(languages, new FileBasedCredentialsProvider("opensubtitle.auth", logger)), + new Subscene(languages))) { + var resultReporter = new QueueProcessReporter(); + var subSyncWorkerProvider = new WorkerProvider(logger, subtitleExtensions, fallbackSubtitleProvider, resultReporter); + var subSyncWorkerQueue = new WorkerQueue(subSyncWorkerProvider, resultReporter); - logger.WriteLine("╔═════════════════════════════════════════════════════╗"); - logger.WriteLine("║ @whi@SubSync v" + version.PadRight(30 - version.Length) + "@gray@ ║"); - logger.WriteLine("║ --------------------------------------- ║"); - logger.WriteLine("║ Copyright (c) 2018 zerratar\\@gmail.com ║"); - logger.WriteLine("╚═════════════════════════════════════════════════════╝"); - //logger.WriteLine("║╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗╔╗║"); - //logger.WriteLine("╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝╚╝"); - logger.WriteLine(""); - logger.WriteLine(" Following folder and its subfolders being watched"); - logger.WriteLine($" @whi@{input} @gray@"); - logger.WriteLine(""); - logger.WriteLine(" You may press @green@'q' @gray@at any time to quit."); - logger.WriteLine(""); - logger.WriteLine(" ───────────────────────────────────────────────────── "); - logger.WriteLine(""); - - mediaWatcher.Start(); - ConsoleKeyInfo ck; - while ((ck = Console.ReadKey(true)).Key != ConsoleKey.Q) + using (var mediaWatcher = new SubtitleSynchronizer(logger, subSyncWorkerQueue, resultReporter, input, videoExtensions, subtitleExtensions)) { - if (ck.Key == ConsoleKey.A) + logger.WriteLine("╔════════════════════════════════════════════╗"); + logger.WriteLine("║ @whi@SubSync v" + version.PadRight(30 - version.Length) + "@gray@ ║"); + logger.WriteLine("║ -------------------------------------- ║"); + logger.WriteLine("║ Copyright (c) 2018 zerratar\\@gmail.com ║"); + logger.WriteLine("╚════════════════════════════════════════════╝"); + logger.WriteLine(""); + logger.WriteLine(" Following folder and its subfolders being watched"); + logger.WriteLine($" @whi@{input} @gray@"); + logger.WriteLine(""); + logger.WriteLine(" You may press @green@'q' @gray@at any time to quit."); + logger.WriteLine(""); + logger.WriteLine(" ───────────────────────────────────────────────────── "); + logger.WriteLine(""); + + mediaWatcher.Start(); + ConsoleKeyInfo ck; + while ((ck = Console.ReadKey(true)).Key != ConsoleKey.Q) { - mediaWatcher.SyncAll(); + if (ck.Key == ConsoleKey.A) + { + mediaWatcher.SyncAll(); + } + System.Threading.Thread.Sleep(10); } - System.Threading.Thread.Sleep(10); } - mediaWatcher.Stop(); } } diff --git a/src/SubSync/SubSync.csproj b/src/SubSync/SubSync.csproj index 9882b1e..b10a134 100644 --- a/src/SubSync/SubSync.csproj +++ b/src/SubSync/SubSync.csproj @@ -3,8 +3,8 @@ Exe netcoreapp2.0 - 0.1.3 - 0.1.3.0 + 0.1.4 + 0.1.4.0 @@ -19,4 +19,10 @@ + + + PreserveNewest + + + diff --git a/src/SubSync/SubSync.csproj.DotSettings b/src/SubSync/SubSync.csproj.DotSettings index 27e5f14..67f7475 100644 --- a/src/SubSync/SubSync.csproj.DotSettings +++ b/src/SubSync/SubSync.csproj.DotSettings @@ -1,5 +1,7 @@  True + True + True True True True \ No newline at end of file diff --git a/src/SubSync/SubtitleProviders/OpenSubtitles.cs b/src/SubSync/SubtitleProviders/OpenSubtitles.cs index 29c5fdf..d9b47f4 100644 --- a/src/SubSync/SubtitleProviders/OpenSubtitles.cs +++ b/src/SubSync/SubtitleProviders/OpenSubtitles.cs @@ -1,23 +1,467 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; namespace SubSync { - internal class OpenSubtitles : SubtitleProviderBase + internal class OpenSubtitles : SubtitleProviderBase, IDisposable { + private const string VipApiUrl = "https://vip-api.opensubtitles.org/xml-rpc"; + private const string ApiUrl = "http://api.opensubtitles.org/xml-rpc"; + + + + private const int MaxDownloadsPerDay = 200; + private const int VipMaxDownloadsPerDay = 1000; + + private const int MaxRequestsEvery10Seconds = 40; + private const int VipMaxRequestsEvery10Seconds = 40; + + private readonly IAuthCredentialProvider credentialProvider; + private readonly HashSet supportedLanguages; + private readonly Thread keepAliveThread; + + private AutoResetEvent loginMutex = new AutoResetEvent(true); + + private DateTime startTime; + private DateTime requestBlockTimeLimit; + + private int keepAliveInterval = 60 * 14; // every 14 minutes, 15 according to api. but just to be safe. + + private int totalRequests; + private int totalRequestsToday; + private int totalRequestsInTimeBlock; + private int downloadQuota = 200; // should be updated from the response header: "Download-Quota" if available. Otherwise manually track. + + private string authenticationToken; + private bool isAuthenticated; + private bool isVip; + + private bool disposed; + + // https://www.opensubtitles.org - public OpenSubtitles(HashSet languages) : base(languages) + public OpenSubtitles(HashSet languages, IAuthCredentialProvider credentialProvider) : base(languages) + { + this.credentialProvider = credentialProvider; + this.supportedLanguages = GetSupportedLanguages(languages); + // until we get our user agent registered for OpenSubtitles.org + // we can use a temporary one. + // See: http://trac.opensubtitles.org/projects/opensubtitles/wiki/DevReadFirst + UserAgent = "TemporaryUserAgent"; + RequestRetryLimit = 3; // max 3 retries, and with some seconds delay is necessary for opensubtitles + startTime = DateTime.Now.Date; + + // Max 40 requests per 10 seconds per IP + // Max 200 subtitle downloads per 24 hour per IP/User + // User has to register as VIP to download 1000 per 24 hours. + // We will have to keep track on requests and downloads for this provider to not exceed the limit and first rely on other providers such as subscene + keepAliveThread = new Thread(KeepAliveProcess); + keepAliveThread.Start(); + } + + private HashSet GetSupportedLanguages(HashSet languages) + { + var result = new HashSet(); + foreach (var language in languages) + { + result.Add(SubtitleLanguage.Find(language)); + } + return result; + } + + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + if (this.isAuthenticated) + { + LogoutAsync(); + } + + this.keepAliveThread.Join(); + } + + public override async Task GetAsync(string name, string outputDirectory) { + AssertWithinRequestLimits(); + + await LoginIfRequiredAsync(); + + var searchResults = await SearchSubtitleAsync(name); + if (searchResults.Length == 0) + { + throw new SubtitleNotFoundException(); + } + + var bestMatchingResult = FindBestSearchResultMatch(name, searchResults); + if (bestMatchingResult == null) + { + throw new SubtitleNotFoundException(); + } + + return await DownloadSubtitleAsync(name, bestMatchingResult); } - protected override int RequestRetryLimit { get; } = 1; + // see http://trac.opensubtitles.org/projects/opensubtitles/wiki/XmlRpcSearchSubtitles + private async Task SearchSubtitleAsync(string name) + { + var languageList = string.Join(",", supportedLanguages.Select(x => x.LanguageId).ToArray()); + + // preferred one + // var movieHash = CalculateVideoHash(name); + // var movieByteSize = GetVideoByteSize(name); + + var query = Path.GetFileName(name); + + var season = ""; + var episode = ""; + var seasonNumber = 0; + var episodeNumber = 0; + var isTvShowEpisode = false; + + var regex = new Regex(@"([s](?\d+)[e](?\d+))|((?\d+)[s](?\d+)) + |((?\d+)[e](?\d+))|([s](?\d+))|(ep(?\d+)) + |(season.(?\d+))|(episode.(?\d+))|(e(?\d+))", + RegexOptions.IgnoreCase); + + foreach (Match m in regex.Matches(query)) + { + var episodeGroup = m.Groups["episode"]; + if (episodeGroup.Success && string.IsNullOrEmpty(episode)) + { + var item = episodeGroup.Captures.FirstOrDefault(); + if (item != null && !string.IsNullOrEmpty(item.Value)) + { + episode = item.Value; + } + } + + var seasonGroup = m.Groups["season"]; + if (seasonGroup.Success && string.IsNullOrEmpty(season)) + { + var item = seasonGroup.Captures.FirstOrDefault(); + if (item != null && !string.IsNullOrEmpty(item.Value)) + { + season = item.Value; + } + } + + if (!string.IsNullOrEmpty(episode) && !string.IsNullOrEmpty(season)) + { + isTvShowEpisode = true; + int.TryParse(episode, out episodeNumber); + int.TryParse(season, out seasonNumber); + break; + } + } + + XmlRpcObject requestResult = null; + if (isTvShowEpisode) + { + query = regex.Replace(query, ""); + requestResult = await ApiRequest("SearchSubtitles", + Arg("query", query), + Arg("sublanguageid", languageList), + Arg("seriesepisode", episodeNumber), + Arg("serieseason", seasonNumber)); + } + else + { + requestResult = await ApiRequest("SearchSubtitles", + Arg("query", query), + Arg("sublanguageid", languageList)); + } - protected override int RequestTimeout { get; } = 3000; + return requestResult.Deserialize("data"); + } + + private Task DownloadSubtitleAsync(string name, Subtitle target) + { + return null; + } + + private Subtitle FindBestSearchResultMatch(string name, Subtitle[] searchResults) + { + // TODO(Zerratar): implement an appropriate matching algorithm + return searchResults.FirstOrDefault(); // super dumb just now. + } + + private async Task LoginIfRequiredAsync() + { + loginMutex.WaitOne(); + + if (!isAuthenticated) + { + try + { + var credentials = credentialProvider.Get(); + var authResult = await LoginAsync(credentials); + authenticationToken = authResult.GetValue("token"); + if (!string.IsNullOrEmpty(authenticationToken)) + { + isAuthenticated = true; + return; + } + } + finally + { + loginMutex.Set(); + } + } + + throw new UnauthorizedAccessException("Login to opensubtitle.org failed!"); + } + + private Task LogoutAsync() + { + return ApiRequest("LogOut"); + } + + private async Task NoOperationAsync() + { + var result = await ApiRequest("NoOperation"); + if (!result.GetValue("status").StartsWith("200")) + { + isAuthenticated = false; + isVip = false; + return false; + } + + return true; + } + + private Task LoginAsync(AuthCredentials credentials) + { + this.authenticationToken = null; + this.isAuthenticated = false; + this.isVip = false; + return ApiRequest("LogIn", Arg(credentials.Username), Arg(credentials.Password), Arg("en"), Arg(UserAgent)); + } + + private async Task ApiRequest(string method, params KeyValuePair[] arguments) + { + Interlocked.Increment(ref totalRequests); + Interlocked.Increment(ref totalRequestsToday); + Interlocked.Increment(ref totalRequestsInTimeBlock); + + if (requestBlockTimeLimit == DateTime.MinValue) + { + requestBlockTimeLimit = DateTime.Now; + } + + var url = isVip ? VipApiUrl : ApiUrl; + var requestData = BuildRequestData(method, arguments); + var request = CreatePostAsync(url, requestData); + try + { + using (var response = await request.GetResponseAsync()) + { + if (response.Headers.HasKeys()) + { + var headerKeys = response.Headers.AllKeys; + if (headerKeys.Contains("Content-Location")) + { + isVip = isVip || response.Headers.Get("Content-Location")?.ToLower() == + "https://vip-api.opensubtitles.org.local/xml-rpc"; + } + + if (headerKeys.Contains("Download-Quota")) + { + if (int.TryParse(response.Headers.Get("Download-Quota"), out var quota)) + { + Volatile.Write(ref downloadQuota, quota); + } + + } + } + + return XmlRpcObject.Parse(await GetResponseStringAsync(response)); + } + } + catch (Exception exc) + { + // for now... + return null; + } + } + + private void KeepAliveProcess() + { + var lastKeepAliveMessage = DateTime.Now; + while (!disposed) + { + if (this.isAuthenticated) + { + if (DateTime.Now - lastKeepAliveMessage > TimeSpan.FromSeconds(keepAliveInterval)) + { + lastKeepAliveMessage = DateTime.Now; + // ignore the fact that its fire-and-forget, since we will only do this every 14 mins, which is more than enough time for this to return a result. + NoOperationAsync(); + } + } + Thread.Sleep(250); + } + } + + private void AssertWithinRequestLimits() + { + var elapsedTime = DateTime.Now.Date - startTime; + if (elapsedTime >= TimeSpan.FromDays(1)) + { + // its been one day. Reset the counters + Volatile.Write(ref totalRequestsToday, 0); + Volatile.Write(ref downloadQuota, isVip ? VipMaxDownloadsPerDay : MaxDownloadsPerDay); + startTime = DateTime.Now.Date; + } + else + { + var quota = Volatile.Read(ref downloadQuota); + if (quota <= 0) + { + throw new DownloadQuotaReachedException(); + } + + var timeBlock = DateTime.Now - requestBlockTimeLimit; + if (timeBlock < TimeSpan.FromSeconds(10)) + { + var requests = Volatile.Read(ref totalRequestsInTimeBlock); + if (requests >= (isVip ? VipMaxRequestsEvery10Seconds : MaxRequestsEvery10Seconds)) + { + throw new RequestQuotaReachedException(); + } + } + else + { + requestBlockTimeLimit = DateTime.Now; + Volatile.Write(ref totalRequestsInTimeBlock, 0); + } + } + } + + private string BuildRequestData(string method, params KeyValuePair[] arguments) + { + var sb = new StringBuilder(); + sb.Append($"{method}"); + + var tokenRequest = this.isAuthenticated && !string.IsNullOrEmpty(authenticationToken); + if (tokenRequest) + { + sb.Append($"{authenticationToken}"); + if (arguments.Length > 0) + { + sb.Append($""); + foreach (var item in arguments) + { + var argumentType = GetArgumentTypeName(item.Value); + sb.Append($"{item.Key}<{argumentType}>{item.Value}"); + } + sb.Append($""); + } + } + else + { + foreach (var item in arguments) + { + var argumentType = GetArgumentTypeName(item.Value); + sb.Append($"<{argumentType}>{item.Value}"); + } + } + sb.AppendLine(""); + return sb.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static KeyValuePair Arg(string key, object value) + { + return new KeyValuePair(key, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static KeyValuePair Arg(object value) + { + return new KeyValuePair("", value); + } + + private static string GetArgumentTypeName(object item) + { + if (item is string) return "string"; + if (item is double) return "double"; + if (item is int) return "int"; + return "string"; + } + + private static byte[] ComputeMovieHash(string filename) + { + byte[] result; + using (Stream input = File.OpenRead(filename)) + { + result = ComputeMovieHash(input); + } + return result; + } + + private static byte[] ComputeMovieHash(Stream input) + { + long lhash, streamsize; + streamsize = input.Length; + lhash = streamsize; + + long i = 0; + byte[] buffer = new byte[sizeof(long)]; + while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) + { + i++; + lhash += BitConverter.ToInt64(buffer, 0); + } + + input.Position = Math.Max(0, streamsize - 65536); + i = 0; + while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) + { + i++; + lhash += BitConverter.ToInt64(buffer, 0); + } + input.Close(); + byte[] result = BitConverter.GetBytes(lhash); + Array.Reverse(result); + return result; + } + + private static string ToHexadecimal(byte[] bytes) + { + StringBuilder hexBuilder = new StringBuilder(); + for (int i = 0; i < bytes.Length; i++) + { + hexBuilder.Append(bytes[i].ToString("x2")); + } + return hexBuilder.ToString(); + } - public override Task GetAsync(string name, string outputDirectory) + internal class Subtitle { - throw new NotImplementedException(); + public string IdSubtitleFile { get; set; } + public string SubFileName { get; set; } + public string SubLanguageId { get; set; } + public string LanguageName { get; set; } + public string MovieReleaseName { get; set; } + public string MovieName { get; set; } + public string SubEncoding { get; set; } + public string SubDownloadLink { get; set; } + public string ZipDownloadLink { get; set; } + public string SubtitleLink { get; set; } + public double SubRating { get; set; } } } } \ No newline at end of file diff --git a/src/SubSync/SubtitleProviders/Subscene.cs b/src/SubSync/SubtitleProviders/Subscene.cs index debe0f3..2d645b7 100644 --- a/src/SubSync/SubtitleProviders/Subscene.cs +++ b/src/SubSync/SubtitleProviders/Subscene.cs @@ -14,10 +14,6 @@ public Subscene(HashSet languages) : base(languages) { } - protected override int RequestRetryLimit { get; } = 3; - - protected override int RequestTimeout { get; } = 3000; - public override async Task GetAsync(string name, string outputDirectory) { var url = await FindAsync(name); diff --git a/src/SubSync/opensubtitles.auth b/src/SubSync/opensubtitles.auth new file mode 100644 index 0000000..8c1c392 --- /dev/null +++ b/src/SubSync/opensubtitles.auth @@ -0,0 +1,2 @@ +username= +password= \ No newline at end of file diff --git a/tests/SubSync.Tests/SubSync.Tests.csproj b/tests/SubSync.Tests/SubSync.Tests.csproj new file mode 100644 index 0000000..96b7d72 --- /dev/null +++ b/tests/SubSync.Tests/SubSync.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + diff --git a/tests/SubSync.Tests/UnitTest1.cs b/tests/SubSync.Tests/UnitTest1.cs new file mode 100644 index 0000000..bda7c65 --- /dev/null +++ b/tests/SubSync.Tests/UnitTest1.cs @@ -0,0 +1,32 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SubSync.Tests +{ + [TestClass] + public class XmlRpcObjectTests + { + [TestMethod] + public void ParseLoginResponse() + { + string input = + "tokenEHBCnqCU4eVW7z1cKLmo0GdTn12status200 OKseconds0.008"; + var obj = XmlRpcObject.Parse(input); + + Assert.IsNotNull(obj); + + var token = obj.GetValue("token"); + Assert.AreEqual("EHBCnqCU4eVW7z1cKLmo0GdTn12", token); + + } + + [TestMethod] + public void ParseSearchResponse() + { + string input = + "status200 OKdataMatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466753SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.HI.srtSubActualCD1SubSize106135SubHash377f0d270c22d472758385e66a48af31SubLastTS01:54:38SubTSGroup222InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886229UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:30:48SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt224098MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashb94a959d4666c92c8cdd2cb9f3837118SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19d90c5c/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466753.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5760bc2/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886229SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886229/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score27.24098MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466754SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.srtSubActualCD1SubSize89920SubHash7523385fd25312350e5bebb0dc873352SubLastTS01:54:38SubTSGroup224InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886230UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:30:51SubBad0SubRating10.0SubSumVotes2SubDownloadsCnt120356MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash60252b3296b29c1fc932bfde46393449SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19da0c5d/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466754.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f56f0bba/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886230SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886230/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score26.20356MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466757SubFileNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVO.srtSubActualCD1SubSize89920SubHash73aaa7d6cae77a919ae15e6beb264689SubLastTS01:54:38SubTSGroup227InfoReleaseGroupEVOInfoFormatWEB-DLInfoOtherIDSubtitle6886233UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:46:54SubBad0SubRating7.0SubSumVotes1SubDownloadsCnt129020MovieReleaseNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVOMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashc21f5f99c7b4ba8db34c62a15655afe4SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dd0c60/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466757.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5720bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886233SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886233/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score25.6902MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466695SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS-HI.srtSubActualCD1SubSize105014SubHashd97fcd839d3b47a7f2eae829d6c51208SubLastTS01:54:38SubTSGroup215InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886170UserID1568273SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 02:18:23SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt16332MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameLuis-subsSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRanktrustedSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash3fca2115185408ddc5747f3c6dcfa32fSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e00c61/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466695.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5740bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886170SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886170/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score25.16332MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466696SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.srtSubActualCD1SubSize88884SubHash69e202eb99a5e4236b02aacb24a2fc86SubLastTS01:54:38SubTSGroup217InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886171UserID1568273SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 02:18:34SubBad0SubRating8.0SubSumVotes4SubDownloadsCnt29292MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameLuis-subsSubTranslatorISO639enLanguageNameEnglishSubComments1SubHearingImpaired0UserRanktrustedSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash10ccc7c93930cc00eacfdbe0fd8c03bfSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e10c62/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466696.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5750bbe/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886171SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886171/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score24.89292MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955469470SubFileNameDoctor.Strange.2016.BluRay.1080P.DTS.x264-CHD.eng.srtSubActualCD1SubSize85223SubHashb987e93c243303277b56f2e2bf22473dSubLastTS01:54:38SubTSGroup255InfoReleaseGroupCHDInfoFormatBluRayInfoOtherIDSubtitle6888930UserID1295723SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-17 19:46:27SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt70716MovieReleaseName Doctor.Strange.2016.BluRay.1080P.DTS.x264-CHDMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamedeep_seaSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRanktrustedSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dd0c5b/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955469470.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f58c0bc3/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6888930SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6888930/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score23.70716MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466756SubFileNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVO.HI.srtSubActualCD1SubSize106135SubHasha9f6603b439a926ed12762a31603c791SubLastTS01:54:38SubTSGroup225InfoReleaseGroupEVOInfoFormatWEB-DLInfoOtherIDSubtitle6886232UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:46:50SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt20532MovieReleaseNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVOMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashb6fd539c1b4d5a0104cc46749847272bSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dc0c5f/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466756.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5710bbc/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886232SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886232/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score23.20532MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955448118SubFileNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCage-ENG.srtSubActualCD1SubSize81267SubHash03f8490dd3ef0e055f5c5f37e859097cSubLastTS01:54:33SubTSGroup121InfoReleaseGroup950MB.MkvCageInfoFormatHDTVInfoOtherIDSubtitle6867701UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-01-29 00:39:39SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt489035MovieReleaseNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCageMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashd7f2a5447244afb71fdbc628c09767e8SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19c20c57/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955448118.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5730bbc/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6867701SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6867701/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score17.89035MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955374037SubFileNameDoctor.Strange.2016.HDTS.750MB.MkvCage.srtSubActualCD1SubSize77513SubHash9ac63e078218aa5bb8db443d1f7e2001SubLastTS01:49:03SubTSGroup41InfoReleaseGroup750MB.MkvCageInfoFormatTelesyncInfoOtherIDSubtitle6793844UserID1638754SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2016-11-12 17:19:40SubBad0SubRating5.0SubSumVotes1SubDownloadsCnt198312MovieReleaseName Doctor.Strange.2016.HDTS.750MB.MkvCageMovieFPS29.970IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamewbibizaSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD0SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashac5f4ba471b5d97ca38035c6b24d761aSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19bb0c55/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955374037.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f57a0bc2/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6793844SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6793844/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score13.98312MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955467815SubFileNameDoctor.Strange.2016.1080p.BluRay.HEVC.x265-GIRAYS-ENG.srtSubActualCD1SubSize81812SubHash1825145b74ba3e1a58b7aa43e7c171eeSubLastTS01:54:42SubTSGroup253InfoReleaseGroupGIRAYSInfoFormatBluRayInfoOtherIDSubtitle6887289UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-16 03:21:31SubBad0SubRating9.4SubSumVotes5SubDownloadsCnt101473MovieReleaseNameDoctor.Strange.2016.1080p.BluRay.HEVC.x265-GIRAYSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash9fd0da94d4691167ba5db849b66cdac4SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19da0c5c/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955467815.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5860bc9/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6887289SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6887289/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score13.89473MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955451144SubFileNameDoctor.Strange.2016.DVDScr.XVID.AC3.HQ.Hive-CM8.srtSubActualCD1SubSize81455SubHash53216f199b1f8801e656bea010b0b9d3SubLastTSSubTSGroupInfoReleaseGroupInfoFormatInfoOtherIDSubtitle6870456UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-01-31 01:29:00SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt82047MovieReleaseNameDoctor.Strange.2016.DVDScr.XVID.AC3.HQ.Hive-CM8MovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments1SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD0SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ad0c50/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955451144.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5620bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6870456SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6870456/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score13.82047MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955470033SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.srtSubActualCD1SubSize89370SubHashfa0d8f91cac40c8bfab3f9426bac7ce1SubLastTS01:54:40SubTSGroup255InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6889502UserID4008162SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-18 04:15:18SubBad0SubRating8.5SubSumVotes2SubDownloadsCnt16071MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameLeSaigneurSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ad0c4e/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955470033.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5800bbf/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6889502SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6889502/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score12.86071MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955458737SubFileNameDoctor.Strange.2016.WEB-DL.720p.x264-HK-ENG.srtSubActualCD1SubSize81512SubHash93db904cd86a115aa143a88721cc8757SubLastTS01:50:04SubTSGroup137InfoReleaseGroupInfoFormatWEB-DLInfoOtherIDSubtitle6878253UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-07 23:10:42SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt70807MovieReleaseNameDoctor.Strange.2016.WEB-DL.720p.x264-HKMovieFPS25.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash48ab7507a3e654da3dc50a76e648623aSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dc0c5f/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955458737.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5790bc0/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6878253SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6878253/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.70807MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955486309SubFileNameDoctor Strange.srtSubActualCD1SubSize116734SubHashca080a0fceacb66fb6571863d99e5c08SubLastTS01:54:38SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle6905778UserID4787991SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-03-04 23:15:08SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt16979MovieReleaseNameDoctor Strange BRrip, Bluray,BDRipMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameJOKER74AHMEDSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankplatinum memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19d30c5b/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955486309.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5680bc3/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6905778SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6905778/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.16979MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955518150SubFileNameDoctor Strange (Director\'s Commentary).en.srtSubActualCD1SubSize171970SubHashc3de4bb3c6e817ad8efd96036b5fc4b9SubLastTS01:54:38SubTSGroup255InfoReleaseGroupDirector\'s CommentaryInfoFormatInfoOtherIDSubtitle6937522UserID5197717SubLanguageIDengSubFormatsrtSubSumCD2SubAuthorCommentSubAddDate2017-04-04 09:05:17SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt7704MovieReleaseNameDoctor Strange (Director\'s Commentary)MovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameJOKER-74-AHMEDSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankplatinum memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19b90c51/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955518150.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5690bbb/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6937522SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6937522/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.07704MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955518151SubFileNameDoctor Strange.en.Blu-Ray.srtSubActualCD2SubSize99351SubHash5e1ecdfdb050d8f4c0b6e01fef21f6cdSubLastTS01:54:38SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle6937522UserID5197717SubLanguageIDengSubFormatsrtSubSumCD2SubAuthorCommentSubAddDate2017-04-04 09:05:17SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt7704MovieReleaseNameDoctor Strange (Director\'s Commentary)MovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameJOKER-74-AHMEDSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankplatinum memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash2ccd21717dfe6006a59459299187dc7aSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ba0c52/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955518151.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5690bbb/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6937522SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6937522/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.07704MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955677259SubFileNameDoctor Strange (2016).1.srtSubActualCD1SubSize106335SubHashb46f66014314fef073574c15acee42cdSubLastTS01:49:02SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle7096302UserID3880240SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-09-15 07:57:39SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt7269MovieReleaseName Doctor Strange (2016).1MovieFPS29.970IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamemaple_leavesSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e50c61/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955677259.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f54a0bb4/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/7096302SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/7096302/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.07269MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955372910SubFileNameDoctor.Strange.2016.HD-TS.x264.AC3-CPG.srtSubActualCD1SubSize76171SubHashb1f189f58ffc15808621975208f5f34fSubLastTS01:47:21SubTSGroup39InfoReleaseGroupCPGInfoFormatTelesyncInfoOtherIDSubtitle6792726UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2016-11-11 19:44:38SubBad0SubRating7.0SubSumVotes1SubDownloadsCnt854032MovieReleaseNameDoctor.Strange.2016.HD.TS.x264.AC3.CPGMovieFPS24.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD0SeriesIMDBParent0SubEncodingCP1252SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash3587a1dca7d790ddc7a60cc12b45bf54SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19c30c53/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955372910.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5710bc0/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6792726SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6792726/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score9.94032MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955445300SubFileNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCage.srtSubActualCD1SubSize77513SubHash783f34affa1755d1137f5935985312bbSubLastTS01:49:01SubTSGroup62InfoReleaseGroup950MB.MkvCageInfoFormatHDTVInfoOtherIDSubtitle6864906UserID3923077SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-01-26 05:21:03SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt375499MovieReleaseNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCageMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamerosinibujangSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankbronze memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash8175177d8f7817bf7b6f8278a166d11bSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19b20c4d/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955445300.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5720bc0/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6864906SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6864906/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score9.75499MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955462360SubFileNameDoctor.Strange.2016.1080p.HDRip.X264.AC3-EVO.en.srtSubActualCD1SubSize81254SubHashbbd807bc64656f21e35138fa3d2cd28cSubLastTS01:49:57SubTSGroup148InfoReleaseGroupEVOInfoFormatHDTVInfoOtherIDSubtitle6881856UserID3393892SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-11 09:18:04SubBad0SubRating8.5SubSumVotes2SubDownloadsCnt92896MovieReleaseNameDoctor.Strange.2016.1080p.HDRip.X264.AC3-EVOMovieFPS25.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamejoononSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankbronze memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashe03b4f826b1692c9a888b67eff3bb6f1SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19bc0c52/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955462360.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5770bc3/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6881856SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6881856/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score8.62896MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955467597SubFileNameDoctor.Strange.2016.WEB-DL.x264-FGT-resync.srtSubActualCD1SubSize88164SubHash29df6effa918689c806c43b44d60dd4fSubLastTS01:49:53SubTSGroup247InfoReleaseGroupFGT-resyncInfoFormatWEB-DLInfoOtherIDSubtitle6887071UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 22:20:56SubBad0SubRating10.0SubSumVotes2SubDownloadsCnt15601MovieReleaseNameDoctor.Strange.2016.1080p.HDRip.X264.AC3-EVOMovieFPS25.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorTristanISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHasheff0a5c63adffad057f76e066ec934c0SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e30c63/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955467597.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5760bbe/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6887071SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6887071/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score2.15601MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955465426SubFileNameDoctor.Strange.2016.720p.WEB-DL.XviD.AC3-FGT.srtSubActualCD1SubSize87999SubHash2d5470f6bc2d7f57dae3ce684ebc2bfdSubLastTS01:54:38SubTSGroup184InfoReleaseGroupFGTInfoFormatWEB-DLInfoOtherIDSubtitle6884905UserID2497520SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-13 21:26:29SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt16909MovieReleaseName Doctor.Strange.2016.720p.WEB-DL.XviD.AC3-FGTMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashb3517d26c7bed29d10376f7ebffe7b6dSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19c90c58/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955465426.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f57b0bc1/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6884905SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6884905/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score1.16909MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955465609SubFileNameDoctor.Strange.2016.720p.WEB-DL.H264.AC3-EVO.srtSubActualCD1SubSize87869SubHash239a93cadf4b630f1c4a11cca4c6a1aaSubLastTS01:54:38SubTSGroup193InfoReleaseGroupEVOInfoFormatWEB-DLInfoOtherIDSubtitle6885090UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-14 00:31:03SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt13148MovieReleaseNameDoctor.Strange.2016.720p.WEB-DL.H264.AC3-EVOMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashfb6ef9b5972cc4a950708b8c42d63f66SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ce0c5b/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955465609.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5710bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6885090SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6885090/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score0.13148MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955633069SubFileNameDoctor Strange (2016) Doutor Estranho.eng.srtSubActualCD1SubSize88300SubHashe24802e4a327f0079db1a594636a991aSubLastTS01:54:38SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle7052332UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-07-29 14:45:52SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt2847MovieReleaseNameDoctor-Strange-2016-720p-WEB-DL-XviD-AC3-FGTMovieFPS0.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19bd0c58/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955633069.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f52c0baf/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/7052332SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/7052332/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score0.02847seconds0.033"; + var obj = XmlRpcObject.Parse(input); + + Assert.IsNotNull(obj); + } + } +} From 1ba15e8a3798e289356dc53f2041eb3373a2a55c Mon Sep 17 00:00:00 2001 From: zerratar Date: Tue, 10 Apr 2018 23:59:00 +0200 Subject: [PATCH 2/2] Add OpenSubtitles.org Subtitle Provider --- .gitignore | 1 + README.md | 29 ++- publish.bat | 9 + src/SubSync/Core/FilenameDiff.cs | 90 ++++++++++ src/SubSync/Core/SubtitleProviderBase.cs | 2 + src/SubSync/Core/Utilities.cs | 82 +++++++++ src/SubSync/Core/XmlRpc/XmlRpcArray.cs | 8 +- src/SubSync/Core/XmlRpc/XmlRpcMember.cs | 14 +- src/SubSync/Core/XmlRpc/XmlRpcObject.cs | 168 +++++++++-------- src/SubSync/Core/XmlRpc/XmlRpcObjectBase.cs | 142 +++++++++++++++ src/SubSync/Core/XmlRpc/XmlRpcRoot.cs | 48 ----- src/SubSync/Core/XmlRpc/XmlRpcStruct.cs | 4 +- src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs | 4 +- src/SubSync/Program.cs | 5 +- src/SubSync/SubSync.csproj | 14 ++ .../SubtitleProviders/OpenSubtitles.cs | 169 ++++++++---------- tests/SubSync.Tests/NameDiffScoreTests.cs | 27 +++ tests/SubSync.Tests/SubSync.Tests.csproj | 16 +- .../{UnitTest1.cs => XmlRpcObjectTests.cs} | 27 ++- win10-x64.bat | 1 - win10-x86.bat | 1 - 21 files changed, 597 insertions(+), 264 deletions(-) create mode 100644 publish.bat create mode 100644 src/SubSync/Core/FilenameDiff.cs create mode 100644 src/SubSync/Core/Utilities.cs create mode 100644 src/SubSync/Core/XmlRpc/XmlRpcObjectBase.cs delete mode 100644 src/SubSync/Core/XmlRpc/XmlRpcRoot.cs create mode 100644 tests/SubSync.Tests/NameDiffScoreTests.cs rename tests/SubSync.Tests/{UnitTest1.cs => XmlRpcObjectTests.cs} (77%) delete mode 100644 win10-x64.bat delete mode 100644 win10-x86.bat diff --git a/.gitignore b/.gitignore index 940794e..4e13bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ x64/ x86/ bld/ [Bb]in/ +[Bb]uild/ [Oo]bj/ [Ll]og/ diff --git a/README.md b/README.md index 459ce49..ec71e68 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # SubSync Automatically download subtitles for your movies -I hope this little tool comes to help you out as much as it did for me. My girlfriend and I both struggled with using the VLC Sub addon for downloading subtitles, a tool that we previously been using lots! And when that tool stopped working on later days, keep being unresponsive and continuesly crashing VLC, we had to manually look for those darn subtitles on the web and if you have a lot of shows to watch then its a huge pain in the ass. +I hope this little tool comes to help you out as much as it did for me. My girlfriend and I both struggled with using the VLC Sub add-on for downloading subtitles, a tool that we previously been using lots! And when that tool stopped working on later days, keep being unresponsive and continuesly crashing VLC, we had to manually look for those darn subtitles on the web and if you have a lot of shows to watch then its a huge pain in the ass. -SubSync is a tool that will keep your movies folder synchronized with subtitles. That means, if you add a video file to the folder or its subfolders being watched a subtitle will automatically be downloaded for that movie. +SubSync is a tool that will keep your movies folder synchronized with subtitles. That means, if you add a video file to the folder or its sub-folders being watched a subtitle will automatically be downloaded for that movie. It works by using the filename of the movie to determine the name and use that to do a search on subscene.com to find the "first best" subtitle to download. It will then extract the file (if its an archive) and rename it to have the same name as the movie file so it can be quickly recognized as a subtitle using VLC. @@ -60,20 +60,31 @@ Now keep it running in the background. Its not going to hog up your cpu. Its pre ## Tips and tricks Press 'q' at any time to exit SubSync -Press 'a' to try and re-sync any previously unsyced subtitles. Yes the subtitle downloads can randomly fail some times when subscene.com decides you shouldnt be downloading their subtitles too often. +Press 'a' to try and re-sync any previously unsynced subtitles. Yes the subtitle downloads can randomly fail some times when subscene.com decides you shouldn't be downloading their subtitles too often. Oh, and be sure to bring popcorns or your favorite snacks when watching your movies! +## Known issues / To-dos + +The OpenSubtitles provider ignore language priority. + +Add support for http://www.yifysubtitles.com/ + ## Changes +### v0.1.4 +Add support for OpenSubtitles.org and is also now the default subtitle provider for SubSync. subscene.com will still be used but only if no subtitles were found on opensubtitles.org. +Improved subtitle search algorithm, but only for OpenSubtitles.org right now. The subscene provider will be updated in a future version. + +The opensubtitles.org provider has not been properly tested though, so there may still be some bugs that needs to be squished. But initial tests returned great results! + +### v0.1.3 +Use Unicode encoding in the console to properly display all texts. + ### v0.1.2 -Update the usage of all FileInfo and Directoryinfo instances to use the ZetaLongPaths available from here https://github.com/UweKeim/ZetaLongPaths to fix the bug caused by too long paths. +Update the usage of all FileInfo and DirectoryInfo instances to use the ZetaLongPaths available from here https://github.com/UweKeim/ZetaLongPaths to fix the bug caused by too long paths. ### v0.1.1 This version didn't like anyone. ### v0.1.0 -Initial release on github, you know the magical first version - -## Todo -Add support for OpenSubtitles as a subtitle provider -Add support for http://www.yifysubtitles.com/ +Initial release on GitHub, you know the magical first version \ No newline at end of file diff --git a/publish.bat b/publish.bat new file mode 100644 index 0000000..2309b0e --- /dev/null +++ b/publish.bat @@ -0,0 +1,9 @@ +rem rmdir .\build\self-contained-win10-x86 /S /Q +rem rmdir .\build\self-contained-win10-x64 /S /Q +rmdir .\build\win10-x86 /S /Q +rmdir .\build\win10-x64 /S /Q +rem dotnet publish -o ..\..\build\self-contained-win10-x86 -r win10-x86 --self-contained +rem dotnet publish -o ..\..\build\self-contained-win10-x64 -r win10-x64 --self-contained +dotnet publish -o ..\..\build\win10-x86 -r win10-x86 --self-contained false +dotnet publish -o ..\..\build\win10-x64 -r win10-x64 --self-contained false +pause \ No newline at end of file diff --git a/src/SubSync/Core/FilenameDiff.cs b/src/SubSync/Core/FilenameDiff.cs new file mode 100644 index 0000000..a316669 --- /dev/null +++ b/src/SubSync/Core/FilenameDiff.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using SubSync.Extensions; + +namespace SubSync +{ + public static class FilenameDiff + { + public static int IndexOfBestMatch(string needle, string[] haystack) + { + var index = 0; + var scored = new Dictionary(); + var input = Path.GetFileNameWithoutExtension(needle); + + foreach (var item in haystack) + { + if (item.Equals(input, StringComparison.OrdinalIgnoreCase)) + { + return index; // direct match + } + scored[index++] = FilenameDiff.GetDiffScore(input, item); + } + + // score: the lower, the better. + if (scored.Count > 0) + { + return scored.OrderBy(x => x.Value).First().Key; + } + + return -1; + } + + public static T FindBestMatch(string needle, T[] haystack, Func stringComparison) + { + var index = IndexOfBestMatch(needle, haystack.Select(stringComparison).ToArray()); + return index != -1 ? haystack[index] : default(T); + } + + public static double GetDiffScore(string a, string b) + { + // first iteration is to take all distinct values from a and b + // then compare the actual content and score it. + // second iteration takes words into consideration + var c1 = a.ToLower().ToCharArray(); + var c2 = b.ToLower().ToCharArray(); + + var diff = new HashSet(c1); + diff.SymmetricExceptWith(c2); + + var changes = diff.ToArray(); // c1.Intersect(c2).ToArray(); + var score = 0.0; + // different chars have different scoring + // letters are 1.0 + // numbers are 0.75 + // brackets and paranthesis are 0.5 + // spaces are 0.1 + foreach (var change in changes) + { + if (change == '[' || change == ']' || change == '(' || change == ')') score += 0.5; + else if (char.IsDigit(change)) score += 0.75; + else if (change == ' ') score += 0.1; + else score += 1.0; + } + + var l0 = new HashSet(); + a.ToLower().Split(new[] { '.', ' ' }, StringSplitOptions.RemoveEmptyEntries).ForEach(x => l0.Add(x)); + if (l0.Count > 1) l0.Remove(a.ToLower()); + + var l1 = new HashSet(); + b.ToLower().Split(new[] { '.', ' ' }, StringSplitOptions.RemoveEmptyEntries).ForEach(x => l1.Add(x)); + if (l1.Count > 1) l1.Remove(b.ToLower()); + + var l2 = new HashSet(); + l2.UnionWith(l0); + l2.UnionWith(l1); + + for (var i = 0; i < l2.Count; i++) + { + if (!l0.Contains(l2.ElementAt(i))) score++; + if (!l1.Contains(l2.ElementAt(i))) score++; + } + + return score; + } + } +} diff --git a/src/SubSync/Core/SubtitleProviderBase.cs b/src/SubSync/Core/SubtitleProviderBase.cs index 218e53a..ee6e14a 100644 --- a/src/SubSync/Core/SubtitleProviderBase.cs +++ b/src/SubSync/Core/SubtitleProviderBase.cs @@ -172,6 +172,8 @@ private HttpWebRequest CreateRequest(string url, string method = "GET") req.Method = method; req.UserAgent = UserAgent; req.Timeout = req.ReadWriteTimeout = RequestTimeout; + req.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; + req.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip, deflate"); return req; } diff --git a/src/SubSync/Core/Utilities.cs b/src/SubSync/Core/Utilities.cs new file mode 100644 index 0000000..ce90a60 --- /dev/null +++ b/src/SubSync/Core/Utilities.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; + +namespace SubSync +{ + public static class Utilities + { + public static byte[] ComputeMovieHash(string filename) + { + byte[] result; + using (Stream input = File.OpenRead(filename)) + { + result = ComputeMovieHash(input); + } + return result; + } + + public static byte[] ComputeMovieHash(Stream input) + { + long lhash, streamsize; + streamsize = input.Length; + lhash = streamsize; + + long i = 0; + byte[] buffer = new byte[sizeof(long)]; + while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) + { + i++; + lhash += BitConverter.ToInt64(buffer, 0); + } + + input.Position = Math.Max(0, streamsize - 65536); + i = 0; + while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) + { + i++; + lhash += BitConverter.ToInt64(buffer, 0); + } + input.Close(); + byte[] result = BitConverter.GetBytes(lhash); + Array.Reverse(result); + return result; + } + + public static string ToHexadecimal(byte[] bytes) + { + StringBuilder hexBuilder = new StringBuilder(); + for (int i = 0; i < bytes.Length; i++) + { + hexBuilder.Append(bytes[i].ToString("x2")); + } + return hexBuilder.ToString(); + } + + public static string DecompressGzipBase64(string base64) + { + var compressedBytes = Convert.FromBase64String(base64); + var data = GzipDecompress(compressedBytes); + return Encoding.UTF8.GetString(data); + } + + public static byte[] GzipDecompress(byte[] gzip) + { + var buffer = new byte[4096]; + using (var stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress)) + using (var memory = new MemoryStream()) + { + var size = 0; + while ((size = stream.Read(buffer, 0, buffer.Length)) != 0) + { + memory.Write(buffer, 0, size); + } + return memory.ToArray(); + } + } + + } +} diff --git a/src/SubSync/Core/XmlRpc/XmlRpcArray.cs b/src/SubSync/Core/XmlRpc/XmlRpcArray.cs index 97e0055..913c83a 100644 --- a/src/SubSync/Core/XmlRpc/XmlRpcArray.cs +++ b/src/SubSync/Core/XmlRpc/XmlRpcArray.cs @@ -1,15 +1,15 @@ namespace SubSync { - public class XmlRpcArray : XmlRpcObject + public class XmlRpcArray : XmlRpcObjectBase { - public XmlRpcObject[] Items { get; } + public XmlRpcObjectBase[] Items { get; } - public XmlRpcArray(XmlRpcObject[] items) + public XmlRpcArray(XmlRpcObjectBase[] items) { Items = items; } - public override XmlRpcObject FindRecursive(string name) + public override XmlRpcObjectBase FindRecursive(string name) { foreach (var item in Items) { diff --git a/src/SubSync/Core/XmlRpc/XmlRpcMember.cs b/src/SubSync/Core/XmlRpc/XmlRpcMember.cs index b54541d..d862967 100644 --- a/src/SubSync/Core/XmlRpc/XmlRpcMember.cs +++ b/src/SubSync/Core/XmlRpc/XmlRpcMember.cs @@ -1,11 +1,13 @@ -namespace SubSync +using System; + +namespace SubSync { - public class XmlRpcMember : XmlRpcObject + public class XmlRpcMember : XmlRpcObjectBase { public XmlRpcString Name { get; } - public XmlRpcObject Value { get; } + public XmlRpcObjectBase Value { get; } - public XmlRpcMember(XmlRpcString name, XmlRpcObject value) + public XmlRpcMember(XmlRpcString name, XmlRpcObjectBase value) { Name = name; Value = value; @@ -16,9 +18,9 @@ public override string ToString() return "'" + Name + "' => '" + Value + "'"; } - public override XmlRpcObject FindRecursive(string name) + public override XmlRpcObjectBase FindRecursive(string name) { - if (name == this.Name.ToString()) + if (name.Equals(this.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { return this; } diff --git a/src/SubSync/Core/XmlRpc/XmlRpcObject.cs b/src/SubSync/Core/XmlRpc/XmlRpcObject.cs index 99d3336..d466b2f 100644 --- a/src/SubSync/Core/XmlRpc/XmlRpcObject.cs +++ b/src/SubSync/Core/XmlRpc/XmlRpcObject.cs @@ -1,140 +1,132 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Xml.Linq; +using System.Reflection; +using System.Runtime.Serialization; namespace SubSync { - public abstract class XmlRpcObject + public class XmlRpcObject : XmlRpcObjectBase { - protected XmlRpcObject() + internal readonly List children = new List(); + + internal void Add(XmlRpcObjectBase child) { + children.Add(child); } - public static XmlRpcRoot Parse(string data) + public T GetValue(string name) { - var doc = XDocument.Parse(data); - if (doc.Root == null) + if (this.children.Count > 0 && !string.IsNullOrEmpty(name)) { - throw new ArgumentException("Argument is not a valid xml document.", nameof(data)); + foreach (var child in children) + { + var node = child.FindRecursive(name); + if (node is XmlRpcMember member) + { + node = member.Value; + } + if (node is IXmlRpcObjectValue valueNode) + { + return (T)valueNode.GetValue(); + } + } } + return default(T); + } - if (doc.Root.Name == "methodResponse") + public override XmlRpcObjectBase FindRecursive(string name) + { + foreach (var child in children) { - return ParseMethodResponse(doc.Root); + var found = child.FindRecursive(name); + if (found != null) + { + return found; + } } - return new XmlRpcRoot(); + return null; } - private static XmlRpcRoot ParseMethodResponse(XElement doc) + public T Deserialize() { - var values = doc.Element("params"); - if (values == null) + var dataRoot = "data"; + var type = typeof(T); + if (type.IsArray) { - throw new Exception("Xmlrpc response is invalid, missing params element."); + var elementType = type.GetElementType(); + return DeserializeArray(dataRoot, elementType, elementType.GetProperties()); } - var resultObject = new XmlRpcRoot(); - var parameters = values.Elements("param"); - foreach (var param in parameters) + if (type.IsGenericType) { - var result = ParseParam(param); - resultObject.Add(result); + var genericTypeDefinition = type.GetGenericTypeDefinition(); + return DeserializeGeneric(dataRoot, genericTypeDefinition, genericTypeDefinition.GetProperties()); } - return resultObject; + return Deserialize(dataRoot, type.GetProperties(BindingFlags.Public)); } - public abstract XmlRpcObject FindRecursive(string name); + private T Deserialize(string dataRoot, PropertyInfo[] properties) + { + throw new NotImplementedException(); + return default(T); + } - private static XmlRpcObject ParseParam(XElement param) + private T DeserializeGeneric(string dataRoot, Type genericType, PropertyInfo[] properties) { - return ParseValue(param.Element("value")); + throw new NotImplementedException(); + return default(T); } - private static XmlRpcObject ParseValue(XElement value) + private T DeserializeArray(string dataRoot, Type elementType, PropertyInfo[] properties) { - foreach (var elm in value.Elements()) + dynamic list = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)); + if (FindRecursive(dataRoot) is XmlRpcMember member) { - if (elm.Name == "struct") - { - return ParseStruct(elm); - } - if (elm.Name == "array") - { - return ParseArray(elm); - } - if (elm.Name == "string") - { - return ParseString(elm); - } - if (elm.Name == "double") + if (member.Value is XmlRpcArray array) { - return ParseDouble(elm); - } - if (elm.Name == "int") - { - return ParseInt(elm); + foreach (var item in array.Items) + { + dynamic value = Deserialize(elementType, item, properties); + list.Add(value); + } } } - return ParseString(value); - } - - private static XmlRpcInt ParseInt(XElement elm) - { - int.TryParse(elm.Value, out var value); - return new XmlRpcInt(value); - } - - private static XmlRpcDouble ParseDouble(XElement elm) - { - double.TryParse(elm.Value, out var value); - return new XmlRpcDouble(value); - } - - private static XmlRpcObject ParseString(XElement elm) - { - return new XmlRpcString(elm.Value); + return list.ToArray(); } - private static XmlRpcArray ParseArray(XElement elm) + private object Deserialize(Type targetType, XmlRpcObjectBase item, PropertyInfo[] properties) { - var items = new List(); - var dataElement = elm.Element("data"); - var elements = dataElement.Elements("value"); - foreach (var elmData in elements) + if (item is XmlRpcStruct strct) { - var item = ParseValue(elmData); - items.Add(item); + return DeserializeStruct(targetType, strct, properties); } - return new XmlRpcArray(items.ToArray()); + // todo: implement any other types that may be supported like serialize as strings, integers, doubles, etc + throw new NotImplementedException(); + + return default(object); } - private static XmlRpcObject ParseStruct(XElement elm) + private object DeserializeStruct(Type structType, XmlRpcStruct structData, PropertyInfo[] properties) { - var foundMembers = new List(); - var members = elm.Elements("member"); - foreach (var member in members) + var instance = Activator.CreateInstance(structType); + foreach (var prop in properties) { - var name = ParseValue(member.Element("name")) as XmlRpcString; - var value = ParseValue(member.Element("value")); - foundMembers.Add(new XmlRpcMember(name, value)); + if (structData.FindRecursive(prop.Name) is XmlRpcMember member) + { + if (member.Value is IXmlRpcObjectValue val) + { + dynamic v = val.GetValue(); + prop.SetValue(instance, v); + } + } } - return new XmlRpcStruct(foundMembers); - } - - public T Deserialize(string data) - { - // aint gonna write a xml parser today - // 1. parse as xml or xdoc - // 2. get properties of type T - // 3. create instance of T - // 4. assign all properties with values matching same name parsed from data. - // - return default(T); + return instance; } } } \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcObjectBase.cs b/src/SubSync/Core/XmlRpc/XmlRpcObjectBase.cs new file mode 100644 index 0000000..5335309 --- /dev/null +++ b/src/SubSync/Core/XmlRpc/XmlRpcObjectBase.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Design.Serialization; +using System.Xml.Linq; + +namespace SubSync +{ + public abstract class XmlRpcObjectBase + { + protected XmlRpcObjectBase() + { + } + + public static XmlRpcObject Parse(string data) + { + var doc = XDocument.Parse(data); + if (doc.Root == null) + { + throw new ArgumentException("Argument is not a valid xml document.", nameof(data)); + } + + if (doc.Root.Name == "methodResponse") + { + return ParseMethodResponse(doc.Root); + } + + return new XmlRpcObject(); + } + + private static XmlRpcObject ParseMethodResponse(XElement doc) + { + var values = doc.Element("params"); + if (values == null) + { + throw new Exception("Xmlrpc response is invalid, missing params element."); + } + + var resultObject = new XmlRpcObject(); + var parameters = values.Elements("param"); + foreach (var param in parameters) + { + var result = ParseParam(param); + resultObject.Add(result); + } + + return resultObject; + } + + + public abstract XmlRpcObjectBase FindRecursive(string name); + + private static XmlRpcObjectBase ParseParam(XElement param) + { + return ParseValue(param.Element("value")); + } + + private static XmlRpcObjectBase ParseValue(XElement value) + { + foreach (var elm in value.Elements()) + { + if (elm.Name == "struct") + { + return ParseStruct(elm); + } + if (elm.Name == "array") + { + return ParseArray(elm); + } + if (elm.Name == "string") + { + return ParseString(elm); + } + if (elm.Name == "double") + { + return ParseDouble(elm); + } + if (elm.Name == "int") + { + return ParseInt(elm); + } + } + + return ParseString(value); + } + + private static XmlRpcInt ParseInt(XElement elm) + { + int.TryParse(elm.Value, out var value); + return new XmlRpcInt(value); + } + + private static XmlRpcDouble ParseDouble(XElement elm) + { + double.TryParse(elm.Value, out var value); + return new XmlRpcDouble(value); + } + + private static XmlRpcObjectBase ParseString(XElement elm) + { + return new XmlRpcString(elm.Value); + } + + private static XmlRpcArray ParseArray(XElement elm) + { + var items = new List(); + var dataElement = elm.Element("data"); + var elements = dataElement.Elements("value"); + foreach (var elmData in elements) + { + var item = ParseValue(elmData); + items.Add(item); + } + + return new XmlRpcArray(items.ToArray()); + } + + private static XmlRpcObjectBase ParseStruct(XElement elm) + { + var foundMembers = new List(); + var members = elm.Elements("member"); + foreach (var member in members) + { + var name = ParseValue(member.Element("name")) as XmlRpcString; + var value = ParseValue(member.Element("value")); + foundMembers.Add(new XmlRpcMember(name, value)); + } + return new XmlRpcStruct(foundMembers); + } + + public static T Deserialize(string data) + { + // aint gonna write a xml parser today + // 1. parse as xml or xdoc + // 2. get properties of type T + // 3. create instance of T + // 4. assign all properties with values matching same name parsed from data. + // + + return Parse(data).Deserialize(); + } + } +} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcRoot.cs b/src/SubSync/Core/XmlRpc/XmlRpcRoot.cs deleted file mode 100644 index fdf1b92..0000000 --- a/src/SubSync/Core/XmlRpc/XmlRpcRoot.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; - -namespace SubSync -{ - public class XmlRpcRoot : XmlRpcObject - { - internal readonly List children = new List(); - - internal void Add(XmlRpcObject child) - { - children.Add(child); - } - - public T GetValue(string name) - { - if (this.children.Count > 0 && !string.IsNullOrEmpty(name)) - { - foreach (var child in children) - { - var node = child.FindRecursive(name); - if (node is XmlRpcMember member) - { - node = member.Value; - } - if (node is IXmlRpcObjectValue valueNode) - { - return (T)valueNode.GetValue(); - } - } - } - return default(T); - } - - public override XmlRpcObject FindRecursive(string name) - { - foreach (var child in children) - { - var found = child.FindRecursive(name); - if (found != null) - { - return found; - } - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs b/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs index 6a48969..ffce6c9 100644 --- a/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs +++ b/src/SubSync/Core/XmlRpc/XmlRpcStruct.cs @@ -2,7 +2,7 @@ namespace SubSync { - public class XmlRpcStruct : XmlRpcObject + public class XmlRpcStruct : XmlRpcObjectBase { public List Members { get; } @@ -11,7 +11,7 @@ public XmlRpcStruct(List members) Members = members; } - public override XmlRpcObject FindRecursive(string name) + public override XmlRpcObjectBase FindRecursive(string name) { foreach (var item in Members) { diff --git a/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs b/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs index 4d2d15e..96e176e 100644 --- a/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs +++ b/src/SubSync/Core/XmlRpc/XmlRpcValueObject.cs @@ -1,8 +1,8 @@ namespace SubSync { - public abstract class XmlRpcValueObject : XmlRpcObject, IXmlRpcObjectValue + public abstract class XmlRpcValueObject : XmlRpcObjectBase, IXmlRpcObjectValue { - public override XmlRpcObject FindRecursive(string name) + public override XmlRpcObjectBase FindRecursive(string name) { return null; } diff --git a/src/SubSync/Program.cs b/src/SubSync/Program.cs index ad630ff..bd0422b 100644 --- a/src/SubSync/Program.cs +++ b/src/SubSync/Program.cs @@ -3,15 +3,14 @@ using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; - +[assembly: InternalsVisibleTo("SubSync.Tests")] namespace SubSync { class Program { static void Main(string[] args) { - //var input = "./"; - var input = "E:\\filmer"; + var input = "./"; var videoExtensions = ParseList("*.avi;*.mp4;*.mkv;*.mpeg;*.flv;*.webm"); var subtitleExtensions = ParseList("*.srt;*.txt;*.sub;*.idx;*.ssa;*.ass"); var languages = ParseList("english"); diff --git a/src/SubSync/SubSync.csproj b/src/SubSync/SubSync.csproj index b10a134..b1428b9 100644 --- a/src/SubSync/SubSync.csproj +++ b/src/SubSync/SubSync.csproj @@ -5,6 +5,20 @@ netcoreapp2.0 0.1.4 0.1.4.0 + + win10-x64; + win10-x86; + linux-x64; + centos-x64; + centos.7-x64; + rhel-x64; + rhel.6-x64; + rhel.7-x64; + rhel.7.1-x64; + rhel.7.2-x64; + rhel.7.3-x64; + rhel.7.4-x64 + diff --git a/src/SubSync/SubtitleProviders/OpenSubtitles.cs b/src/SubSync/SubtitleProviders/OpenSubtitles.cs index d9b47f4..ac5aaea 100644 --- a/src/SubSync/SubtitleProviders/OpenSubtitles.cs +++ b/src/SubSync/SubtitleProviders/OpenSubtitles.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.MemoryMappedFiles; using System.Linq; using System.Net; using System.Runtime.CompilerServices; @@ -8,33 +9,31 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; +using SubSync.Extensions; namespace SubSync { + /// + // Implementation of the https://www.opensubtitles.org XML-RPC Api + /// internal class OpenSubtitles : SubtitleProviderBase, IDisposable { private const string VipApiUrl = "https://vip-api.opensubtitles.org/xml-rpc"; private const string ApiUrl = "http://api.opensubtitles.org/xml-rpc"; - - - private const int MaxDownloadsPerDay = 200; private const int VipMaxDownloadsPerDay = 1000; - private const int MaxRequestsEvery10Seconds = 40; private const int VipMaxRequestsEvery10Seconds = 40; - + private readonly int keepAliveInterval = 60 * 14; // every 14 minutes, 15 according to api. but just to be safe. + private readonly AutoResetEvent loginMutex = new AutoResetEvent(true); private readonly IAuthCredentialProvider credentialProvider; private readonly HashSet supportedLanguages; private readonly Thread keepAliveThread; - private AutoResetEvent loginMutex = new AutoResetEvent(true); - private DateTime startTime; private DateTime requestBlockTimeLimit; - - private int keepAliveInterval = 60 * 14; // every 14 minutes, 15 according to api. but just to be safe. - private int totalRequests; private int totalRequestsToday; private int totalRequestsInTimeBlock; @@ -46,8 +45,6 @@ internal class OpenSubtitles : SubtitleProviderBase, IDisposable private bool disposed; - - // https://www.opensubtitles.org public OpenSubtitles(HashSet languages, IAuthCredentialProvider credentialProvider) : base(languages) { this.credentialProvider = credentialProvider; @@ -67,16 +64,6 @@ public OpenSubtitles(HashSet languages, IAuthCredentialProvider credenti keepAliveThread.Start(); } - private HashSet GetSupportedLanguages(HashSet languages) - { - var result = new HashSet(); - foreach (var language in languages) - { - result.Add(SubtitleLanguage.Find(language)); - } - return result; - } - public void Dispose() { if (this.disposed) @@ -111,7 +98,7 @@ public override async Task GetAsync(string name, string outputDirectory) throw new SubtitleNotFoundException(); } - return await DownloadSubtitleAsync(name, bestMatchingResult); + return await DownloadSubtitleAsync(bestMatchingResult, outputDirectory); } // see http://trac.opensubtitles.org/projects/opensubtitles/wiki/XmlRpcSearchSubtitles @@ -119,7 +106,10 @@ private async Task SearchSubtitleAsync(string name) { var languageList = string.Join(",", supportedLanguages.Select(x => x.LanguageId).ToArray()); - // preferred one + // TODO: its preferred to do a search with hash rather than filename + // for later, we could try use search with hash first and then do a query search + // if no results were given in the first search. + // var movieHash = CalculateVideoHash(name); // var movieByteSize = GetVideoByteSize(name); @@ -175,7 +165,7 @@ private async Task SearchSubtitleAsync(string name) Arg("query", query), Arg("sublanguageid", languageList), Arg("seriesepisode", episodeNumber), - Arg("serieseason", seasonNumber)); + Arg("Seriesseason", seasonNumber)); } else { @@ -184,18 +174,29 @@ private async Task SearchSubtitleAsync(string name) Arg("sublanguageid", languageList)); } - return requestResult.Deserialize("data"); + return requestResult.Deserialize(); } - private Task DownloadSubtitleAsync(string name, Subtitle target) + private async Task DownloadSubtitleAsync(Subtitle target, string outputDirectory) { - return null; + var quota = Interlocked.Decrement(ref downloadQuota); // is really only counted if the request was successeful. but to be on the safe side. + if (quota <= 0) + { + throw new DownloadQuotaReachedException(); + } + + var result = await ApiRequest("DownloadSubtitles", Arg(target.IdSubtitleFile)); + var subtitles = result.Deserialize(); + var subtitle = subtitles.First(); + var subtitleData = Utilities.DecompressGzipBase64(subtitle.Data); + var outputFileName = Path.Combine(outputDirectory, target.SubFileName); + File.WriteAllText(outputFileName, subtitleData, Encoding.UTF8); + return outputFileName; } - private Subtitle FindBestSearchResultMatch(string name, Subtitle[] searchResults) + private static Subtitle FindBestSearchResultMatch(string name, Subtitle[] searchResults) { - // TODO(Zerratar): implement an appropriate matching algorithm - return searchResults.FirstOrDefault(); // super dumb just now. + return FilenameDiff.FindBestMatch(name, searchResults, x => x.MovieReleaseName); } private async Task LoginIfRequiredAsync() @@ -219,9 +220,9 @@ private async Task LoginIfRequiredAsync() { loginMutex.Set(); } - } - throw new UnauthorizedAccessException("Login to opensubtitle.org failed!"); + throw new UnauthorizedAccessException("Login to opensubtitle.org failed!"); + } } private Task LogoutAsync() @@ -242,7 +243,7 @@ private async Task NoOperationAsync() return true; } - private Task LoginAsync(AuthCredentials credentials) + private Task LoginAsync(AuthCredentials credentials) { this.authenticationToken = null; this.isAuthenticated = false; @@ -250,7 +251,7 @@ private Task LoginAsync(AuthCredentials credentials) return ApiRequest("LogIn", Arg(credentials.Username), Arg(credentials.Password), Arg("en"), Arg(UserAgent)); } - private async Task ApiRequest(string method, params KeyValuePair[] arguments) + private async Task ApiRequest(string method, params KeyValuePair[] arguments) { Interlocked.Increment(ref totalRequests); Interlocked.Increment(ref totalRequestsToday); @@ -287,7 +288,7 @@ private async Task ApiRequest(string method, params KeyValuePair GetSupportedLanguages(HashSet languages) + { + var result = new HashSet(); + foreach (var language in languages) + { + result.Add(SubtitleLanguage.Find(language)); + } + return result; + } + private void AssertWithinRequestLimits() { var elapsedTime = DateTime.Now.Date - startTime; @@ -352,22 +363,37 @@ private void AssertWithinRequestLimits() private string BuildRequestData(string method, params KeyValuePair[] arguments) { + // quick and dirty xml-rpc methodcall serialization + // may do this correctly in the future. but meh var sb = new StringBuilder(); sb.Append($"{method}"); - var tokenRequest = this.isAuthenticated && !string.IsNullOrEmpty(authenticationToken); if (tokenRequest) { sb.Append($"{authenticationToken}"); if (arguments.Length > 0) { - sb.Append($""); - foreach (var item in arguments) + var isStructBody = arguments.Any(x => !string.IsNullOrEmpty(x.Key)); + sb.Append($""); + if (isStructBody) { - var argumentType = GetArgumentTypeName(item.Value); - sb.Append($"{item.Key}<{argumentType}>{item.Value}"); + sb.Append(""); + foreach (var item in arguments) + { + var argumentType = GetArgumentTypeName(item.Value); + sb.Append($"{item.Key}<{argumentType}>{item.Value}"); + } + sb.Append($""); } - sb.Append($""); + else + { + foreach (var item in arguments) + { + var argumentType = GetArgumentTypeName(item.Value); + sb.Append($"<{argumentType}>{item.Value}"); + } + } + sb.Append($""); } } else @@ -402,55 +428,9 @@ private static string GetArgumentTypeName(object item) return "string"; } - private static byte[] ComputeMovieHash(string filename) - { - byte[] result; - using (Stream input = File.OpenRead(filename)) - { - result = ComputeMovieHash(input); - } - return result; - } - - private static byte[] ComputeMovieHash(Stream input) - { - long lhash, streamsize; - streamsize = input.Length; - lhash = streamsize; - - long i = 0; - byte[] buffer = new byte[sizeof(long)]; - while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) - { - i++; - lhash += BitConverter.ToInt64(buffer, 0); - } - - input.Position = Math.Max(0, streamsize - 65536); - i = 0; - while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0)) - { - i++; - lhash += BitConverter.ToInt64(buffer, 0); - } - input.Close(); - byte[] result = BitConverter.GetBytes(lhash); - Array.Reverse(result); - return result; - } - - private static string ToHexadecimal(byte[] bytes) - { - StringBuilder hexBuilder = new StringBuilder(); - for (int i = 0; i < bytes.Length; i++) - { - hexBuilder.Append(bytes[i].ToString("x2")); - } - return hexBuilder.ToString(); - } - - internal class Subtitle + public class Subtitle { + public Subtitle() { } // required for deserialization public string IdSubtitleFile { get; set; } public string SubFileName { get; set; } public string SubLanguageId { get; set; } @@ -461,7 +441,14 @@ internal class Subtitle public string SubDownloadLink { get; set; } public string ZipDownloadLink { get; set; } public string SubtitleLink { get; set; } - public double SubRating { get; set; } + public string SubRating { get; set; } + } + + public class SubtitleData + { + public SubtitleData() { } + public string IdSubtitleFile { get; set; } + public string Data { get; set; } } } } \ No newline at end of file diff --git a/tests/SubSync.Tests/NameDiffScoreTests.cs b/tests/SubSync.Tests/NameDiffScoreTests.cs new file mode 100644 index 0000000..b3b7c62 --- /dev/null +++ b/tests/SubSync.Tests/NameDiffScoreTests.cs @@ -0,0 +1,27 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SubSync.Tests +{ + [TestClass] + public class NameDiffScoreTests + { + [TestMethod] + public void Test1() + { + var score0 = FilenameDiff.GetDiffScore("Greys Anatomy s11e11.mp4", + "Greys.Anatomy.S09.720p.HDTV.X264-DIMENSION.mp4"); + + Assert.AreEqual(15.6, score0); + + var score1 = FilenameDiff.GetDiffScore("Greys.Anatomy.S09.480p.HDTV.x264-mSD.mp4", + "Greys.Anatomy.S09.720p.HDTV.X264-DIMENSION.mp4"); + + Assert.AreEqual(6.5, score1); + + var score2 = FilenameDiff.GetDiffScore("Greys.Anatomy.S09.480p.HDTV.x264-mSD.mp4", + "Greys.Anatomy.S09.480p.HDTV.x264-mSD.mp4"); + + Assert.AreEqual(0, score2); + } + } +} \ No newline at end of file diff --git a/tests/SubSync.Tests/SubSync.Tests.csproj b/tests/SubSync.Tests/SubSync.Tests.csproj index 96b7d72..43588ad 100644 --- a/tests/SubSync.Tests/SubSync.Tests.csproj +++ b/tests/SubSync.Tests/SubSync.Tests.csproj @@ -6,10 +6,20 @@ false + + false + + + + + false + + + - - - + + + diff --git a/tests/SubSync.Tests/UnitTest1.cs b/tests/SubSync.Tests/XmlRpcObjectTests.cs similarity index 77% rename from tests/SubSync.Tests/UnitTest1.cs rename to tests/SubSync.Tests/XmlRpcObjectTests.cs index bda7c65..078229f 100644 --- a/tests/SubSync.Tests/UnitTest1.cs +++ b/tests/SubSync.Tests/XmlRpcObjectTests.cs @@ -8,9 +8,9 @@ public class XmlRpcObjectTests [TestMethod] public void ParseLoginResponse() { - string input = + var input = "tokenEHBCnqCU4eVW7z1cKLmo0GdTn12status200 OKseconds0.008"; - var obj = XmlRpcObject.Parse(input); + var obj = XmlRpcObjectBase.Parse(input); Assert.IsNotNull(obj); @@ -22,11 +22,26 @@ public void ParseLoginResponse() [TestMethod] public void ParseSearchResponse() { - string input = + var input = "status200 OKdataMatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466753SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.HI.srtSubActualCD1SubSize106135SubHash377f0d270c22d472758385e66a48af31SubLastTS01:54:38SubTSGroup222InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886229UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:30:48SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt224098MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashb94a959d4666c92c8cdd2cb9f3837118SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19d90c5c/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466753.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5760bc2/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886229SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886229/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score27.24098MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466754SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.srtSubActualCD1SubSize89920SubHash7523385fd25312350e5bebb0dc873352SubLastTS01:54:38SubTSGroup224InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886230UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:30:51SubBad0SubRating10.0SubSumVotes2SubDownloadsCnt120356MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash60252b3296b29c1fc932bfde46393449SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19da0c5d/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466754.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f56f0bba/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886230SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886230/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score26.20356MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466757SubFileNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVO.srtSubActualCD1SubSize89920SubHash73aaa7d6cae77a919ae15e6beb264689SubLastTS01:54:38SubTSGroup227InfoReleaseGroupEVOInfoFormatWEB-DLInfoOtherIDSubtitle6886233UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:46:54SubBad0SubRating7.0SubSumVotes1SubDownloadsCnt129020MovieReleaseNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVOMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashc21f5f99c7b4ba8db34c62a15655afe4SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dd0c60/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466757.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5720bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886233SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886233/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score25.6902MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466695SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS-HI.srtSubActualCD1SubSize105014SubHashd97fcd839d3b47a7f2eae829d6c51208SubLastTS01:54:38SubTSGroup215InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886170UserID1568273SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 02:18:23SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt16332MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameLuis-subsSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRanktrustedSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash3fca2115185408ddc5747f3c6dcfa32fSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e00c61/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466695.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5740bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886170SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886170/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score25.16332MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466696SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.srtSubActualCD1SubSize88884SubHash69e202eb99a5e4236b02aacb24a2fc86SubLastTS01:54:38SubTSGroup217InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6886171UserID1568273SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 02:18:34SubBad0SubRating8.0SubSumVotes4SubDownloadsCnt29292MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameLuis-subsSubTranslatorISO639enLanguageNameEnglishSubComments1SubHearingImpaired0UserRanktrustedSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash10ccc7c93930cc00eacfdbe0fd8c03bfSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e10c62/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466696.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5750bbe/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886171SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886171/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score24.89292MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955469470SubFileNameDoctor.Strange.2016.BluRay.1080P.DTS.x264-CHD.eng.srtSubActualCD1SubSize85223SubHashb987e93c243303277b56f2e2bf22473dSubLastTS01:54:38SubTSGroup255InfoReleaseGroupCHDInfoFormatBluRayInfoOtherIDSubtitle6888930UserID1295723SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-17 19:46:27SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt70716MovieReleaseName Doctor.Strange.2016.BluRay.1080P.DTS.x264-CHDMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamedeep_seaSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRanktrustedSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dd0c5b/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955469470.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f58c0bc3/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6888930SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6888930/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score23.70716MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955466756SubFileNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVO.HI.srtSubActualCD1SubSize106135SubHasha9f6603b439a926ed12762a31603c791SubLastTS01:54:38SubTSGroup225InfoReleaseGroupEVOInfoFormatWEB-DLInfoOtherIDSubtitle6886232UserID1566989SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 05:46:50SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt20532MovieReleaseNameDoctor.Strange.2016.1080p.WEB-DL.H264.AC3-EVOMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameGoldenBeardSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankadministratorSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted1QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashb6fd539c1b4d5a0104cc46749847272bSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dc0c5f/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955466756.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5710bbc/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6886232SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6886232/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score23.20532MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955448118SubFileNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCage-ENG.srtSubActualCD1SubSize81267SubHash03f8490dd3ef0e055f5c5f37e859097cSubLastTS01:54:33SubTSGroup121InfoReleaseGroup950MB.MkvCageInfoFormatHDTVInfoOtherIDSubtitle6867701UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-01-29 00:39:39SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt489035MovieReleaseNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCageMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashd7f2a5447244afb71fdbc628c09767e8SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19c20c57/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955448118.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5730bbc/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6867701SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6867701/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score17.89035MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955374037SubFileNameDoctor.Strange.2016.HDTS.750MB.MkvCage.srtSubActualCD1SubSize77513SubHash9ac63e078218aa5bb8db443d1f7e2001SubLastTS01:49:03SubTSGroup41InfoReleaseGroup750MB.MkvCageInfoFormatTelesyncInfoOtherIDSubtitle6793844UserID1638754SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2016-11-12 17:19:40SubBad0SubRating5.0SubSumVotes1SubDownloadsCnt198312MovieReleaseName Doctor.Strange.2016.HDTS.750MB.MkvCageMovieFPS29.970IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamewbibizaSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD0SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashac5f4ba471b5d97ca38035c6b24d761aSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19bb0c55/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955374037.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f57a0bc2/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6793844SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6793844/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score13.98312MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955467815SubFileNameDoctor.Strange.2016.1080p.BluRay.HEVC.x265-GIRAYS-ENG.srtSubActualCD1SubSize81812SubHash1825145b74ba3e1a58b7aa43e7c171eeSubLastTS01:54:42SubTSGroup253InfoReleaseGroupGIRAYSInfoFormatBluRayInfoOtherIDSubtitle6887289UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-16 03:21:31SubBad0SubRating9.4SubSumVotes5SubDownloadsCnt101473MovieReleaseNameDoctor.Strange.2016.1080p.BluRay.HEVC.x265-GIRAYSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash9fd0da94d4691167ba5db849b66cdac4SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19da0c5c/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955467815.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5860bc9/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6887289SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6887289/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score13.89473MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955451144SubFileNameDoctor.Strange.2016.DVDScr.XVID.AC3.HQ.Hive-CM8.srtSubActualCD1SubSize81455SubHash53216f199b1f8801e656bea010b0b9d3SubLastTSSubTSGroupInfoReleaseGroupInfoFormatInfoOtherIDSubtitle6870456UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-01-31 01:29:00SubBad0SubRating10.0SubSumVotes1SubDownloadsCnt82047MovieReleaseNameDoctor.Strange.2016.DVDScr.XVID.AC3.HQ.Hive-CM8MovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments1SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD0SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ad0c50/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955451144.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5620bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6870456SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6870456/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score13.82047MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955470033SubFileNameDoctor.Strange.2016.720p.BluRay.x264-SPARKS.srtSubActualCD1SubSize89370SubHashfa0d8f91cac40c8bfab3f9426bac7ce1SubLastTS01:54:40SubTSGroup255InfoReleaseGroupSPARKSInfoFormatBluRayInfoOtherIDSubtitle6889502UserID4008162SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-18 04:15:18SubBad0SubRating8.5SubSumVotes2SubDownloadsCnt16071MovieReleaseNameDoctor.Strange.2016.720p.BluRay.x264-SPARKSMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameLeSaigneurSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ad0c4e/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955470033.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5800bbf/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6889502SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6889502/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score12.86071MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955458737SubFileNameDoctor.Strange.2016.WEB-DL.720p.x264-HK-ENG.srtSubActualCD1SubSize81512SubHash93db904cd86a115aa143a88721cc8757SubLastTS01:50:04SubTSGroup137InfoReleaseGroupInfoFormatWEB-DLInfoOtherIDSubtitle6878253UserID1480692SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-07 23:10:42SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt70807MovieReleaseNameDoctor.Strange.2016.WEB-DL.720p.x264-HKMovieFPS25.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameAsifAkheirSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash48ab7507a3e654da3dc50a76e648623aSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19dc0c5f/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955458737.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5790bc0/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6878253SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6878253/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.70807MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955486309SubFileNameDoctor Strange.srtSubActualCD1SubSize116734SubHashca080a0fceacb66fb6571863d99e5c08SubLastTS01:54:38SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle6905778UserID4787991SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-03-04 23:15:08SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt16979MovieReleaseNameDoctor Strange BRrip, Bluray,BDRipMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameJOKER74AHMEDSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankplatinum memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19d30c5b/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955486309.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5680bc3/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6905778SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6905778/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.16979MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955518150SubFileNameDoctor Strange (Director\'s Commentary).en.srtSubActualCD1SubSize171970SubHashc3de4bb3c6e817ad8efd96036b5fc4b9SubLastTS01:54:38SubTSGroup255InfoReleaseGroupDirector\'s CommentaryInfoFormatInfoOtherIDSubtitle6937522UserID5197717SubLanguageIDengSubFormatsrtSubSumCD2SubAuthorCommentSubAddDate2017-04-04 09:05:17SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt7704MovieReleaseNameDoctor Strange (Director\'s Commentary)MovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameJOKER-74-AHMEDSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankplatinum memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19b90c51/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955518150.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5690bbb/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6937522SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6937522/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.07704MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955518151SubFileNameDoctor Strange.en.Blu-Ray.srtSubActualCD2SubSize99351SubHash5e1ecdfdb050d8f4c0b6e01fef21f6cdSubLastTS01:54:38SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle6937522UserID5197717SubLanguageIDengSubFormatsrtSubSumCD2SubAuthorCommentSubAddDate2017-04-04 09:05:17SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt7704MovieReleaseNameDoctor Strange (Director\'s Commentary)MovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameJOKER-74-AHMEDSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankplatinum memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash2ccd21717dfe6006a59459299187dc7aSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ba0c52/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955518151.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5690bbb/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6937522SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6937522/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.07704MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955677259SubFileNameDoctor Strange (2016).1.srtSubActualCD1SubSize106335SubHashb46f66014314fef073574c15acee42cdSubLastTS01:49:02SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle7096302UserID3880240SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-09-15 07:57:39SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt7269MovieReleaseName Doctor Strange (2016).1MovieFPS29.970IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamemaple_leavesSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired1UserRankgold memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e50c61/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955677259.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f54a0bb4/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/7096302SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/7096302/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score11.07269MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955372910SubFileNameDoctor.Strange.2016.HD-TS.x264.AC3-CPG.srtSubActualCD1SubSize76171SubHashb1f189f58ffc15808621975208f5f34fSubLastTS01:47:21SubTSGroup39InfoReleaseGroupCPGInfoFormatTelesyncInfoOtherIDSubtitle6792726UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2016-11-11 19:44:38SubBad0SubRating7.0SubSumVotes1SubDownloadsCnt854032MovieReleaseNameDoctor.Strange.2016.HD.TS.x264.AC3.CPGMovieFPS24.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD0SeriesIMDBParent0SubEncodingCP1252SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash3587a1dca7d790ddc7a60cc12b45bf54SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19c30c53/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955372910.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5710bc0/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6792726SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6792726/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score9.94032MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955445300SubFileNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCage.srtSubActualCD1SubSize77513SubHash783f34affa1755d1137f5935985312bbSubLastTS01:49:01SubTSGroup62InfoReleaseGroup950MB.MkvCageInfoFormatHDTVInfoOtherIDSubtitle6864906UserID3923077SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-01-26 05:21:03SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt375499MovieReleaseNameDoctor.Strange.2016.720p.HC.HDRip.950MB.MkvCageMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamerosinibujangSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankbronze memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash8175177d8f7817bf7b6f8278a166d11bSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19b20c4d/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955445300.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5720bc0/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6864906SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6864906/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score9.75499MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955462360SubFileNameDoctor.Strange.2016.1080p.HDRip.X264.AC3-EVO.en.srtSubActualCD1SubSize81254SubHashbbd807bc64656f21e35138fa3d2cd28cSubLastTS01:49:57SubTSGroup148InfoReleaseGroupEVOInfoFormatHDTVInfoOtherIDSubtitle6881856UserID3393892SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-11 09:18:04SubBad0SubRating8.5SubSumVotes2SubDownloadsCnt92896MovieReleaseNameDoctor.Strange.2016.1080p.HDRip.X264.AC3-EVOMovieFPS25.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNamejoononSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankbronze memberSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashe03b4f826b1692c9a888b67eff3bb6f1SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19bc0c52/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955462360.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5770bc3/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6881856SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6881856/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score8.62896MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955467597SubFileNameDoctor.Strange.2016.WEB-DL.x264-FGT-resync.srtSubActualCD1SubSize88164SubHash29df6effa918689c806c43b44d60dd4fSubLastTS01:49:53SubTSGroup247InfoReleaseGroupFGT-resyncInfoFormatWEB-DLInfoOtherIDSubtitle6887071UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-15 22:20:56SubBad0SubRating10.0SubSumVotes2SubDownloadsCnt15601MovieReleaseNameDoctor.Strange.2016.1080p.HDRip.X264.AC3-EVOMovieFPS25.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorTristanISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHasheff0a5c63adffad057f76e066ec934c0SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19e30c63/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955467597.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5760bbe/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6887071SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6887071/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score2.15601MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955465426SubFileNameDoctor.Strange.2016.720p.WEB-DL.XviD.AC3-FGT.srtSubActualCD1SubSize87999SubHash2d5470f6bc2d7f57dae3ce684ebc2bfdSubLastTS01:54:38SubTSGroup184InfoReleaseGroupFGTInfoFormatWEB-DLInfoOtherIDSubtitle6884905UserID2497520SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-13 21:26:29SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt16909MovieReleaseName Doctor.Strange.2016.720p.WEB-DL.XviD.AC3-FGTMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashb3517d26c7bed29d10376f7ebffe7b6dSubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19c90c58/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955465426.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f57b0bc1/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6884905SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6884905/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score1.16909MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955465609SubFileNameDoctor.Strange.2016.720p.WEB-DL.H264.AC3-EVO.srtSubActualCD1SubSize87869SubHash239a93cadf4b630f1c4a11cca4c6a1aaSubLastTS01:54:38SubTSGroup193InfoReleaseGroupEVOInfoFormatWEB-DLInfoOtherIDSubtitle6885090UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-02-14 00:31:03SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt13148MovieReleaseNameDoctor.Strange.2016.720p.WEB-DL.H264.AC3-EVOMovieFPS23.976IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHashfb6ef9b5972cc4a950708b8c42d63f66SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19ce0c5b/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955465609.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f5710bbd/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/6885090SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/6885090/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score0.13148MatchedByfulltextIDSubMovieFile0MovieHash0MovieByteSize0MovieTimeMS0IDSubtitleFile1955633069SubFileNameDoctor Strange (2016) Doutor Estranho.eng.srtSubActualCD1SubSize88300SubHashe24802e4a327f0079db1a594636a991aSubLastTS01:54:38SubTSGroup255InfoReleaseGroupInfoFormatInfoOtherIDSubtitle7052332UserID0SubLanguageIDengSubFormatsrtSubSumCD1SubAuthorCommentSubAddDate2017-07-29 14:45:52SubBad0SubRating0.0SubSumVotes0SubDownloadsCnt2847MovieReleaseNameDoctor-Strange-2016-720p-WEB-DL-XviD-AC3-FGTMovieFPS0.000IDMovie165743IDMovieImdb1211837MovieNameDoctor StrangeMovieNameEngMovieYear2016MovieImdbRating7.5SubFeatured0UserNickNameSubTranslatorISO639enLanguageNameEnglishSubComments0SubHearingImpaired0UserRankSeriesSeason0SeriesEpisode0MovieKindmovieSubHD1SeriesIMDBParent0SubEncodingUTF-8SubAutoTranslation0SubForeignPartsOnly0SubFromTrusted0QueryParametersquerydoctor.strange.2016.dvdscr.xvid.ac3.hq.hive-cm8.avisublanguageidengSubTSGroupHash7c64d7a4b7a3ca54ffa79e56cbd47737SubDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-19bd0c58/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/filead/1955633069.gzZipDownloadLinkhttp://dl.opensubtitles.org/en/download/src-api/vrf-f52c0baf/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/subad/7052332SubtitlesLinkhttp://www.opensubtitles.org/en/subtitles/7052332/sid-C7UQ7fitJLKKG-oqOmg1S6n1L6d/doctor-strange-enQueryNumber0Score0.02847seconds0.033"; - var obj = XmlRpcObject.Parse(input); - - Assert.IsNotNull(obj); + var obj = XmlRpcObjectBase.Parse(input); + var subtitles = obj.Deserialize(); + + Assert.IsNotNull(subtitles); + Assert.AreEqual(24, subtitles.Length); + } + + + [TestMethod] + public void ParseDownloadSubtitleResponse() + { + var input = + "status200 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.058"; + var obj = XmlRpcObjectBase.Parse(input); + + var subtitles = obj.Deserialize(); + Assert.IsNotNull(subtitles); + Assert.AreEqual(1, subtitles.Length); } } } diff --git a/win10-x64.bat b/win10-x64.bat deleted file mode 100644 index 44d0c8d..0000000 --- a/win10-x64.bat +++ /dev/null @@ -1 +0,0 @@ -dotnet publish -c Release -r win10-x64 --self-contained \ No newline at end of file diff --git a/win10-x86.bat b/win10-x86.bat deleted file mode 100644 index 8987885..0000000 --- a/win10-x86.bat +++ /dev/null @@ -1 +0,0 @@ -dotnet publish -c Release -r win10-x86 --self-contained \ No newline at end of file