├── .gitignore
├── .vidignore
├── LICENSE
├── README.md
├── SubSync.sln
├── build.bat
├── src
├── SubSync
│ ├── App.config
│ ├── Program.cs
│ ├── SubSync.csproj
│ ├── SubSync.csproj.DotSettings
│ ├── opensubtitles.auth
│ └── packages.config
├── SubSync462
│ ├── App.config
│ ├── Program.cs
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ ├── SubSync462.csproj
│ └── packages.config
└── SubSyncLib
│ ├── Arguments.cs
│ ├── Logic
│ ├── AuthCredentials.cs
│ ├── ConsoleLogger.cs
│ ├── Exceptions
│ │ ├── DownloadQuotaReachedException.cs
│ │ ├── NestedArchiveNotSupportedException.cs
│ │ ├── RequestQuotaReachedException.cs
│ │ └── SubtitleNotFoundException.cs
│ ├── Extensions
│ │ └── EnumerableExtensions.cs
│ ├── FallbackSubtitleProvider.cs
│ ├── FileBasedCredentialsProvider.cs
│ ├── FilenameDiff.cs
│ ├── IAuthCredentialProvider.cs
│ ├── IFileSystemWatcher.cs
│ ├── ILogger.cs
│ ├── IStatusReporter.cs
│ ├── ISubtitleProvider.cs
│ ├── IVideoIgnoreFilter.cs
│ ├── IVideoSyncList.cs
│ ├── IWorker.cs
│ ├── IWorkerProvider.cs
│ ├── IWorkerQueue.cs
│ ├── QueueCompletedEventArgs.cs
│ ├── QueueProcessReporter.cs
│ ├── QueueProcessResult.cs
│ ├── SubtitleLanguage.cs
│ ├── SubtitleProviderBase.cs
│ ├── SubtitleSynchronizer.cs
│ ├── Utilities.cs
│ ├── VideoFormats
│ │ ├── IVideoHeader.cs
│ │ └── MatroskaVideo.cs
│ ├── VideoIgnoreFilter.cs
│ ├── VideoSyncList.cs
│ ├── Worker.cs
│ ├── WorkerProvider.cs
│ ├── WorkerQueue.cs
│ ├── WorkerStatus.cs
│ └── XmlRpc
│ │ ├── IXmlRpcObjectValue.cs
│ │ ├── XmlRpcArray.cs
│ │ ├── XmlRpcDouble.cs
│ │ ├── XmlRpcInt.cs
│ │ ├── XmlRpcMember.cs
│ │ ├── XmlRpcObject.cs
│ │ ├── XmlRpcObjectBase.cs
│ │ ├── XmlRpcString.cs
│ │ ├── XmlRpcStruct.cs
│ │ └── XmlRpcValueObject.cs
│ ├── Program.cs
│ ├── Providers
│ ├── OpenSubtitles.cs
│ └── Subscene.cs
│ └── SubSyncLib.csproj
└── tests
└── SubSync.Tests
├── FilterTests.cs
├── NameDiffScoreTests.cs
├── SubSync.Tests.csproj
└── XmlRpcObjectTests.cs
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Bb]uild/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # .NET Core
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 | **/Properties/launchSettings.json
51 |
52 | *_i.c
53 | *_p.c
54 | *_i.h
55 | *.ilk
56 | *.meta
57 | *.obj
58 | *.pch
59 | *.pdb
60 | *.pgc
61 | *.pgd
62 | *.rsp
63 | *.sbr
64 | *.tlb
65 | *.tli
66 | *.tlh
67 | *.tmp
68 | *.tmp_proj
69 | *.log
70 | *.vspscc
71 | *.vssscc
72 | .builds
73 | *.pidb
74 | *.svclog
75 | *.scc
76 |
77 | # Chutzpah Test files
78 | _Chutzpah*
79 |
80 | # Visual C++ cache files
81 | ipch/
82 | *.aps
83 | *.ncb
84 | *.opendb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 | *.VC.db
89 | *.VC.VC.opendb
90 |
91 | # Visual Studio profiler
92 | *.psess
93 | *.vsp
94 | *.vspx
95 | *.sap
96 |
97 | # TFS 2012 Local Workspace
98 | $tf/
99 |
100 | # Guidance Automation Toolkit
101 | *.gpState
102 |
103 | # ReSharper is a .NET coding add-in
104 | _ReSharper*/
105 | *.[Rr]e[Ss]harper
106 | *.DotSettings.user
107 |
108 | # JustCode is a .NET coding add-in
109 | .JustCode
110 |
111 | # TeamCity is a build add-in
112 | _TeamCity*
113 |
114 | # DotCover is a Code Coverage Tool
115 | *.dotCover
116 |
117 | # Visual Studio code coverage results
118 | *.coverage
119 | *.coveragexml
120 |
121 | # NCrunch
122 | _NCrunch_*
123 | .*crunch*.local.xml
124 | nCrunchTemp_*
125 |
126 | # MightyMoose
127 | *.mm.*
128 | AutoTest.Net/
129 |
130 | # Web workbench (sass)
131 | .sass-cache/
132 |
133 | # Installshield output folder
134 | [Ee]xpress/
135 |
136 | # DocProject is a documentation generator add-in
137 | DocProject/buildhelp/
138 | DocProject/Help/*.HxT
139 | DocProject/Help/*.HxC
140 | DocProject/Help/*.hhc
141 | DocProject/Help/*.hhk
142 | DocProject/Help/*.hhp
143 | DocProject/Help/Html2
144 | DocProject/Help/html
145 |
146 | # Click-Once directory
147 | publish/
148 |
149 | # Publish Web Output
150 | *.[Pp]ublish.xml
151 | *.azurePubxml
152 | # TODO: Comment the next line if you want to checkin your web deploy settings
153 | # but database connection strings (with potential passwords) will be unencrypted
154 | *.pubxml
155 | *.publishproj
156 |
157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
158 | # checkin your Azure Web App publish settings, but sensitive information contained
159 | # in these scripts will be unencrypted
160 | PublishScripts/
161 |
162 | # NuGet Packages
163 | *.nupkg
164 | # The packages folder can be ignored because of Package Restore
165 | **/packages/*
166 | # except build/, which is used as an MSBuild target.
167 | !**/packages/build/
168 | # Uncomment if necessary however generally it will be regenerated when needed
169 | #!**/packages/repositories.config
170 | # NuGet v3's project.json files produces more ignorable files
171 | *.nuget.props
172 | *.nuget.targets
173 |
174 | # Microsoft Azure Build Output
175 | csx/
176 | *.build.csdef
177 |
178 | # Microsoft Azure Emulator
179 | ecf/
180 | rcf/
181 |
182 | # Windows Store app package directories and files
183 | AppPackages/
184 | BundleArtifacts/
185 | Package.StoreAssociation.xml
186 | _pkginfo.txt
187 |
188 | # Visual Studio cache files
189 | # files ending in .cache can be ignored
190 | *.[Cc]ache
191 | # but keep track of directories ending in .cache
192 | !*.[Cc]ache/
193 |
194 | # Others
195 | ClientBin/
196 | ~$*
197 | *~
198 | *.dbmdl
199 | *.dbproj.schemaview
200 | *.jfm
201 | *.pfx
202 | *.publishsettings
203 | orleans.codegen.cs
204 |
205 | # Since there are multiple workflows, uncomment next line to ignore bower_components
206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
207 | #bower_components/
208 |
209 | # RIA/Silverlight projects
210 | Generated_Code/
211 |
212 | # Backup & report files from converting an old project file
213 | # to a newer Visual Studio version. Backup files are not needed,
214 | # because we have git ;-)
215 | _UpgradeReport_Files/
216 | Backup*/
217 | UpgradeLog*.XML
218 | UpgradeLog*.htm
219 |
220 | # SQL Server files
221 | *.mdf
222 | *.ldf
223 | *.ndf
224 |
225 | # Business Intelligence projects
226 | *.rdl.data
227 | *.bim.layout
228 | *.bim_*.settings
229 |
230 | # Microsoft Fakes
231 | FakesAssemblies/
232 |
233 | # GhostDoc plugin setting file
234 | *.GhostDoc.xml
235 |
236 | # Node.js Tools for Visual Studio
237 | .ntvs_analysis.dat
238 | node_modules/
239 |
240 | # Typescript v1 declaration files
241 | typings/
242 |
243 | # Visual Studio 6 build log
244 | *.plg
245 |
246 | # Visual Studio 6 workspace options file
247 | *.opt
248 |
249 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
250 | *.vbw
251 |
252 | # Visual Studio LightSwitch build output
253 | **/*.HTMLClient/GeneratedArtifacts
254 | **/*.DesktopClient/GeneratedArtifacts
255 | **/*.DesktopClient/ModelManifest.xml
256 | **/*.Server/GeneratedArtifacts
257 | **/*.Server/ModelManifest.xml
258 | _Pvt_Extensions
259 |
260 | # Paket dependency manager
261 | .paket/paket.exe
262 | paket-files/
263 |
264 | # FAKE - F# Make
265 | .fake/
266 |
267 | # JetBrains Rider
268 | .idea/
269 | *.sln.iml
270 |
271 | # CodeRush
272 | .cr/
273 |
274 | # Python Tools for Visual Studio (PTVS)
275 | __pycache__/
276 | *.pyc
277 |
278 | # Cake - Uncomment if you are using it
279 | # tools/**
280 | # !tools/packages.config
281 |
282 | # Telerik's JustMock configuration file
283 | *.jmconfig
284 |
285 | # BizTalk build output
286 | *.btp.cs
287 | *.btm.cs
288 | *.odx.cs
289 | *.xsd.cs
290 |
--------------------------------------------------------------------------------
/.vidignore:
--------------------------------------------------------------------------------
1 | # works similar to .gitignore
2 | # one row per ignore, you can use asterisks for wildcard
3 | # this will tell SubSync to ignore paths / videos that you don't want synced
4 | # or just because they don't seem to succeed.
5 |
6 | cabin fever*/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Karl Patrik Johansson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SubSync
2 | Automatically download subtitles for your movies
3 |
4 | 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.
5 |
6 | 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.
7 |
8 | 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.
9 |
10 | SubSync will also prefer a chosen language over the other, so for me I prefer Swedish but if that one does not exist I want English, you can set this as a startup argument.
11 |
12 | **Note:**
13 | This tool was created with just a couple of hours so be aware that it may not always work perfectly.
14 | I do appreciate any problems you may stumble upon either code-wise or functionality when/or if testing this application out. Don't be afraid to add an issue!
15 |
16 | ## Binaries
17 | You can download the somewhat latest binaries from the release tab thingy
18 | https://github.com/zerratar/SubSync/releases
19 |
20 | Mirror mirror on the wall
21 | http://www.shinobytes.com/files/SubSync-binaries-win32.zip
22 |
23 | ## Building SubSync
24 | Load up Visual Studio 2017, open SubSync.sln and hit CTRL+SHIFT+B like your life depends on it!
25 |
26 | ## Running SubSync
27 | ```batch
28 | SubSync.exe [-lang ] [-vid ] [-sub ] [-delay ] [-resync] [-resyncall] [-exit]
29 |
30 | : So this is the folder you want to watch, it will also watch all its subfolders.
31 | As an example: 'D:\Movies'
32 |
33 | -lang
34 | : A list of languages separated by a semi-colon. Example: swedish;english
35 | The priority of the languages is from left to right, so in this example if
36 | a swedish translation subtitle is available it will take that one rather
37 | than the english one.
38 |
39 | The value is just English per default.
40 |
41 | -vid
42 | : A list of video extensions to watch seperated by a semi-colon, just add
43 | all you can think of. But this is an optional and default value is:
44 | *.avi;*.mp4;*.mkv;*.mpeg;*.flv;*.webm
45 |
46 | -sub
47 | : A list of recognizable subtitle files, formatted same way as video extensions
48 | since I don't know of all possible extensions I've made this an optional parameter
49 | to change which subtitles SubSync should recognize. The default value is:
50 | *.srt;*.txt;*.sub;*.idx;*.ssa;*.ass
51 |
52 | -delay
53 | : If you want to have a delay between each request by a set of milliseconds you can use this flag.
54 | This will then run all request sequential instead of concurrent which will take a lot longer to download
55 | all subtitles. However by adding a delay you have a greater chance of the server giving you a proper response
56 | and therefor can make the download more stable.
57 | The value is set to 0 as default and therefor disabled.
58 |
59 | -resync: Downloads subtitles for all your videos that has not been previously synchronized with SubSync
60 | regardless of whether they already have a subtitle file or not.
61 | Warning: This will overwrite any existing subtitles you may already have.
62 |
63 | -resyncall: As -resync, it downloads all subtitles again but this one also downloads all subtitles SubSync
64 | has previously flagged as synced.
65 | Warning: This will overwrite any existing subtitles you may already have.
66 |
67 | -exit: If you don't want to keep SubSync running, you can have it automatically exit as soon as its done syncing
68 | your subtitles, remember this will exit even if there are no subtitles to sync.
69 | ```
70 |
71 | In most cases you will probably only need to run it like this:
72 |
73 | ```batch
74 | SubSync.exe "D:\My Awesome Movies\"
75 | ```
76 |
77 | Or if you want to have your subtitles in another language (if one exists) and you're
78 | crazy enough to think you will want Latin before English.
79 |
80 | ```batch
81 | SubSync.exe "D:\My Awesome Movies\" -lang spanish;japanese;latin;english
82 | ```
83 |
84 | Now keep it running in the background. Its not going to hog up your cpu. Its pretty friendly, and you can be sure to have subtitles available for you whenever you need it!
85 |
86 | ## Tips and tricks
87 | **Exiting**
88 | Press 'q' at any time to exit SubSync.
89 | Or you can supply the argument --exit to automatically close SubSync down when its done downloading subtitles.
90 |
91 | **Retrying**
92 | Press 'a' to try and re-sync any previously unsynced subtitles. Yes the subtitle downloads can randomly fail some times when the subtitle providers decides you shouldn't be downloading their subtitles too often.
93 |
94 | **ignoring videos**
95 | There is a file called .vidignore, it is a list of 1 entry per row that is being used by SubSync to completely ignore a video and not
96 | download any subtitles for that particular video. This can be handy when SubSync keeps failing on some videos and you don't want it
97 | to keep trying.
98 |
99 | The patterns are similar to .gitignore, but this only support wildcards.
100 |
101 | Oh, and be sure to bring popcorns or your favorite snacks when watching your movies!
102 |
103 | ## Known issues / To-dos
104 |
105 | The OpenSubtitles provider ignore language priority.
106 |
107 | Add support for http://www.yifysubtitles.com/
108 |
109 | ## Changes
110 | ### v0.1.6.2
111 | Reworked the startup arguments and added an --exit flag that can be used to force quit SubSync after first sync.
112 |
113 | ### v0.1.4
114 | 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.
115 | Improved subtitle search algorithm, but only for OpenSubtitles.org right now. The subscene provider will be updated in a future version.
116 |
117 | 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!
118 |
119 | ### v0.1.3
120 | Use Unicode encoding in the console to properly display all texts.
121 |
122 | ### v0.1.2
123 | 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.
124 |
125 | ### v0.1.1
126 | This version didn't like anyone.
127 |
128 | ### v0.1.0
129 | Initial release on GitHub, you know the magical first version
--------------------------------------------------------------------------------
/SubSync.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.2011
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SubSync", "src\SubSync\SubSync.csproj", "{A1B8C57E-2B99-4D21-A35E-D050F3BA279B}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C182B254-972A-4CB2-8002-1653E0F24323}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7D3E5130-833D-4426-9E7C-7D91CBA217B3}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SubSync.Tests", "tests\SubSync.Tests\SubSync.Tests.csproj", "{71C95A54-2D86-4AC9-AE79-05CF93E063EA}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SubSyncLib", "src\SubSyncLib\SubSyncLib.csproj", "{EFA46430-AEE1-47B4-89C9-80A94CF4649D}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SubSync462", "src\SubSync462\SubSync462.csproj", "{17EDD74B-FEBF-4817-93F7-3E3221B32B18}"
17 | EndProject
18 | Global
19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
20 | Debug|Any CPU = Debug|Any CPU
21 | Release|Any CPU = Release|Any CPU
22 | EndGlobalSection
23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
24 | {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {A1B8C57E-2B99-4D21-A35E-D050F3BA279B}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {71C95A54-2D86-4AC9-AE79-05CF93E063EA}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {EFA46430-AEE1-47B4-89C9-80A94CF4649D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {EFA46430-AEE1-47B4-89C9-80A94CF4649D}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {EFA46430-AEE1-47B4-89C9-80A94CF4649D}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {EFA46430-AEE1-47B4-89C9-80A94CF4649D}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {17EDD74B-FEBF-4817-93F7-3E3221B32B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {17EDD74B-FEBF-4817-93F7-3E3221B32B18}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {17EDD74B-FEBF-4817-93F7-3E3221B32B18}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {17EDD74B-FEBF-4817-93F7-3E3221B32B18}.Release|Any CPU.Build.0 = Release|Any CPU
40 | EndGlobalSection
41 | GlobalSection(SolutionProperties) = preSolution
42 | HideSolutionNode = FALSE
43 | EndGlobalSection
44 | GlobalSection(NestedProjects) = preSolution
45 | {A1B8C57E-2B99-4D21-A35E-D050F3BA279B} = {C182B254-972A-4CB2-8002-1653E0F24323}
46 | {71C95A54-2D86-4AC9-AE79-05CF93E063EA} = {7D3E5130-833D-4426-9E7C-7D91CBA217B3}
47 | {EFA46430-AEE1-47B4-89C9-80A94CF4649D} = {C182B254-972A-4CB2-8002-1653E0F24323}
48 | {17EDD74B-FEBF-4817-93F7-3E3221B32B18} = {C182B254-972A-4CB2-8002-1653E0F24323}
49 | EndGlobalSection
50 | GlobalSection(ExtensibilityGlobals) = postSolution
51 | SolutionGuid = {1EBFA09F-C84B-4414-AAF2-998BD08F6A97}
52 | EndGlobalSection
53 | EndGlobal
54 |
--------------------------------------------------------------------------------
/build.bat:
--------------------------------------------------------------------------------
1 | dotnet build
--------------------------------------------------------------------------------
/src/SubSync/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/SubSync/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("SubSync.Tests")]
4 | namespace SubSync
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | SubSyncLib.Program.Main(args);
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/SubSync/SubSync.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.0
6 | 0.1.6.2
7 | 0.1.6.1
8 |
9 | win10-x64;
10 | win10-x86;
11 | linux-x64;
12 | centos-x64;
13 | centos.7-x64;
14 | rhel-x64;
15 | rhel.6-x64;
16 | rhel.7-x64;
17 | rhel.7.1-x64;
18 | rhel.7.2-x64;
19 | rhel.7.3-x64;
20 | rhel.7.4-x64
21 |
22 |
23 |
24 |
25 | ..\..\build\netcoreapp2.0\
26 |
27 |
28 |
29 | ..\..\build
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | PreserveNewest
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/SubSync/SubSync.csproj.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
4 | True
5 | True
6 | True
7 | True
--------------------------------------------------------------------------------
/src/SubSync/opensubtitles.auth:
--------------------------------------------------------------------------------
1 | username=
2 | password=
--------------------------------------------------------------------------------
/src/SubSync/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/SubSync462/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/SubSync462/Program.cs:
--------------------------------------------------------------------------------
1 | namespace SubSync462
2 | {
3 | class Program
4 | {
5 | static void Main(string[] args)
6 | {
7 | SubSyncLib.Program.Main(args);
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/SubSync462/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("SubSync")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("SubSync")]
13 | [assembly: AssemblyCopyright("Copyright © Shinobytes 2018")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("17edd74b-febf-4817-93f7-3e3221b32b18")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("0.1.6.2")]
36 | [assembly: AssemblyFileVersion("0.1.6.2")]
37 |
--------------------------------------------------------------------------------
/src/SubSync462/SubSync462.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {17EDD74B-FEBF-4817-93F7-3E3221B32B18}
8 | Exe
9 | SubSync
10 | SubSync
11 | v4.6.2
12 | 512
13 | true
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | ..\..\build\net462\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 | ..\..\packages\SharpCompress.0.21.0\lib\net45\SharpCompress.dll
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {efa46430-aee1-47b4-89c9-80a94cf4649d}
58 | SubSyncLib
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/SubSync462/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Arguments.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Globalization;
5 | using System.Linq;
6 | using System.Reflection;
7 | using System.Runtime.CompilerServices;
8 |
9 | namespace SubSyncLib
10 | {
11 | public static class Arguments
12 | {
13 | public static T Parse(string[] args) where T : new()
14 | {
15 | args = ApplyQuoteBugFix(args);
16 |
17 | var settings = new T();
18 | var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
19 | foreach (var property in properties)
20 | {
21 | var arg = property.GetCustomAttribute();
22 | if (arg != null)
23 | {
24 | var argName = $"-{arg.ArgumentName}";
25 | var isFlag = property.PropertyType == typeof(bool);
26 | if (isFlag)
27 | {
28 | var index = Array.FindIndex(args, x => x.EndsWith(argName, StringComparison.OrdinalIgnoreCase));
29 | if (index != -1)
30 | {
31 | SetArgumentValue(property, settings, true);
32 | }
33 | continue;
34 | }
35 |
36 | if (arg.UseArgumentName)
37 | {
38 | var index = Array.FindIndex(args, x => x.EndsWith(argName, StringComparison.OrdinalIgnoreCase));
39 | if (index == -1 || index + 1 >= args.Length)
40 | {
41 | if (!string.IsNullOrEmpty(arg.DefaultValue))
42 | {
43 | SetArgumentValue(property, settings, arg.DefaultValue);
44 | }
45 | continue;
46 | }
47 |
48 | SetArgumentValueByIndex(args, index + 1, property, ref settings);
49 | }
50 | else
51 | {
52 | if (arg.ArgumentIndex >= args.Length)
53 | {
54 | if (!string.IsNullOrEmpty(arg.DefaultValue))
55 | {
56 | SetArgumentValue(property, settings, arg.DefaultValue);
57 | }
58 | continue;
59 | }
60 |
61 | SetArgumentValueByIndex(args, arg.ArgumentIndex, property, ref settings);
62 | }
63 | }
64 | }
65 |
66 | return settings;
67 | }
68 |
69 | private static string[] ApplyQuoteBugFix(string[] args)
70 | {
71 | var newArgs = new List();
72 | foreach (var arg in args)
73 | {
74 | string tmpArg = "";
75 | if (arg.Contains("\""))
76 | {
77 | // anything afterwards will be part of the string, so we have to break up this string
78 | // by its spaces.
79 | var values = arg.Split('"');
80 | newArgs.Add(values[0]);
81 | tmpArg = values[1];
82 | }
83 | else if (arg.Contains("'"))
84 | {
85 | var values = arg.Split('\'');
86 | newArgs.Add(values[0]);
87 | tmpArg = values[1];
88 | }
89 | else newArgs.Add(arg);
90 |
91 | if (!string.IsNullOrEmpty(tmpArg))
92 | {
93 | newArgs.AddRange(tmpArg.Split(' '));
94 | }
95 | }
96 |
97 | return newArgs.ToArray();
98 | }
99 |
100 | private static void SetArgumentValueByIndex(
101 | string[] args,
102 | int index,
103 | PropertyInfo property,
104 | ref T settings) where T : new()
105 | {
106 | var value = args[index];
107 | SetArgumentValue(property, settings, value);
108 | }
109 |
110 | private static void SetArgumentValue(PropertyInfo property, T settings, string value)
111 | {
112 | if (property.PropertyType == typeof(int))
113 | {
114 | if (int.TryParse(value, out var v))
115 | property.SetValue(settings, v);
116 | }
117 | else if (property.PropertyType == typeof(bool))
118 | {
119 | if (bool.TryParse(value, out var v))
120 | property.SetValue(settings, v);
121 | }
122 | else if (property.PropertyType == typeof(string))
123 | {
124 | property.SetValue(settings, value);
125 | }
126 | else if (property.PropertyType == typeof(HashSet))
127 | {
128 | property.SetValue(settings, ParseList(value));
129 | }
130 | else
131 | {
132 | try
133 | {
134 | var tc = TypeDescriptor.GetConverter(property.PropertyType);
135 | var objValue = tc.ConvertFromString(null, CultureInfo.InvariantCulture, value);
136 | property.SetValue(settings, objValue, null);
137 | }
138 | catch { }
139 | }
140 | }
141 |
142 | private static void SetArgumentValue(PropertyInfo property, T settings, object value)
143 | {
144 | property.SetValue(settings, value);
145 | }
146 |
147 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
148 | private static HashSet ParseList(string s)
149 | {
150 | return new HashSet(s
151 | .Split(';')
152 | .Select(x => x.Trim())
153 | .Select(x => x.StartsWith("*") ? x.Substring(1) : x));
154 | }
155 | }
156 |
157 | public class StartupArgumentAttribute : Attribute
158 | {
159 | public StartupArgumentAttribute(string argName, string defaultValue = null)
160 | {
161 | ArgumentName = argName;
162 | DefaultValue = defaultValue;
163 | }
164 |
165 | public StartupArgumentAttribute(int argIndex, string defaultValue = null)
166 | {
167 | ArgumentIndex = argIndex;
168 | DefaultValue = defaultValue;
169 | }
170 |
171 | public int ArgumentIndex { get; set; } = -1;
172 | public string DefaultValue { get; }
173 | public string ArgumentName { get; set; }
174 | public bool UseArgumentName => !string.IsNullOrEmpty(ArgumentName) || ArgumentIndex == -1;
175 | }
176 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/AuthCredentials.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public struct AuthCredentials
4 | {
5 | public readonly string Username;
6 | public readonly string Password;
7 |
8 | public AuthCredentials(string username, string password)
9 | {
10 | Username = username;
11 | Password = password;
12 | }
13 |
14 | public bool IsEmpty => string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(Password);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/ConsoleLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace SubSyncLib.Logic
7 | {
8 | public class ConsoleLogger : ILogger
9 | {
10 | private readonly object writelock = new object();
11 |
12 | public ConsoleLogger()
13 | {
14 | Console.OutputEncoding = Encoding.Unicode;
15 | }
16 |
17 | public void Write(string message)
18 | {
19 | WriteOperations(ParseMessageOperations(" " + message));
20 | }
21 |
22 | public void WriteLine(string message)
23 | {
24 | WriteLineOperations(ParseMessageOperations(" " + message));
25 | }
26 |
27 | public void Debug(string message)
28 | {
29 | #if DEBUG
30 | WriteLine($"[@{ConsoleColor.Cyan}@DBG@{ConsoleColor.Gray}@] {message}");
31 | #endif
32 | }
33 |
34 | public void Error(string errorMessage)
35 | {
36 | WriteLine($"@{ConsoleColor.Red}@{errorMessage}");
37 | }
38 |
39 | private void WriteLineOperations(IReadOnlyList operations)
40 | {
41 | WriteOperations(operations, true);
42 | }
43 |
44 | private void WriteOperations(IReadOnlyList operations, bool newLine = false)
45 | {
46 | lock (writelock)
47 | {
48 | var prevForeground = Console.ForegroundColor;
49 | var prevBackground = Console.BackgroundColor;
50 | foreach (var op in operations)
51 | {
52 | Console.ForegroundColor = op.ForegroundColor;
53 | Console.BackgroundColor = op.BackgroundColor;
54 | Console.Write(op.Text);
55 | }
56 | Console.ForegroundColor = prevForeground;
57 | Console.BackgroundColor = prevBackground;
58 | if (newLine)
59 | {
60 | Console.WriteLine();
61 | }
62 | }
63 | }
64 |
65 | private IReadOnlyList ParseMessageOperations(string message)
66 | {
67 | var ops = new List();
68 | var tokens = Tokenize(message);
69 | var tokenIndex = 0;
70 |
71 | var foregroundColor = Console.ForegroundColor;
72 | var backgroundColor = Console.BackgroundColor;
73 | while (tokenIndex < tokens.Count)
74 | {
75 | var token = tokens[tokenIndex];
76 | switch (token.Type)
77 | {
78 | case TextTokenType.At:
79 | {
80 | var prev = tokens[tokenIndex - 1];
81 | var prevOp = ops[ops.Count - 1];
82 | if (prev.Text.EndsWith("\\"))
83 | {
84 | ops[ops.Count - 1] = new ConsoleWriteOperation(prevOp.Text.Remove(prevOp.Text.Length - 1), prevOp.ForegroundColor, prevOp.BackgroundColor);
85 | goto default;
86 | }
87 | foregroundColor = ParseColor(tokens[++tokenIndex].Text);
88 | ++tokenIndex;// var endToken = tokens[++tokenIndex];
89 | }
90 | break;
91 | case TextTokenType.Hash:
92 | {
93 | var prev = tokens[tokenIndex - 1];
94 | var prevOp = ops[ops.Count - 1];
95 | if (prev.Text.EndsWith("\\"))
96 | {
97 | ops[ops.Count - 1] = new ConsoleWriteOperation(prevOp.Text.Remove(prevOp.Text.Length - 1), prevOp.ForegroundColor, prevOp.BackgroundColor);
98 | goto default;
99 | }
100 | backgroundColor = ParseColor(tokens[++tokenIndex].Text);
101 | ++tokenIndex;// var endToken = tokens[++tokenIndex];
102 | }
103 | break;
104 | default:
105 | ops.Add(new ConsoleWriteOperation(token.Text, foregroundColor, backgroundColor));
106 | break;
107 | }
108 | tokenIndex++;
109 | }
110 | return ops;
111 | }
112 |
113 | private static ConsoleColor ParseColor(string color)
114 | {
115 | if (int.TryParse(color, out var value))
116 | {
117 | return (ConsoleColor)value;
118 | }
119 |
120 | // ex: @white@
121 | var names = Enum.GetNames(typeof(ConsoleColor));
122 | var possibleColorName = names.FirstOrDefault(x => x.Equals(color, StringComparison.OrdinalIgnoreCase));
123 | if (possibleColorName != null)
124 | {
125 | return Enum.GetValues(typeof(ConsoleColor))
126 | .Cast()
127 | .ElementAt(Array.IndexOf(names, possibleColorName));
128 | }
129 |
130 | // ex: @whi@
131 | possibleColorName = names.FirstOrDefault(x => x.StartsWith(color, StringComparison.OrdinalIgnoreCase));
132 | if (possibleColorName != null)
133 | {
134 | return Enum.GetValues(typeof(ConsoleColor))
135 | .Cast()
136 | .ElementAt(Array.IndexOf(names, possibleColorName));
137 | }
138 |
139 | return Console.ForegroundColor;
140 | }
141 |
142 | private IReadOnlyList Tokenize(string message)
143 | {
144 | var tokens = new List();
145 | var index = 0;
146 | while (index < message.Length)
147 | {
148 | var token = message[index];
149 | switch (token)
150 | {
151 | case '@':
152 | tokens.Add(new TextToken(TextTokenType.At, "@"));
153 | break;
154 | case '#':
155 | tokens.Add(new TextToken(TextTokenType.Hash, "#"));
156 | break;
157 | default:
158 | {
159 | var str = token.ToString();
160 | while (index + 1 < message.Length)
161 | {
162 | var next = message[index + 1];
163 | if (next == '@' || next == '#') break;
164 | str += message[++index];
165 | }
166 | tokens.Add(new TextToken(TextTokenType.Text, str));
167 | break;
168 | }
169 | }
170 |
171 | index++;
172 | }
173 | return tokens;
174 | }
175 |
176 | private struct TextToken
177 | {
178 | public readonly string Text;
179 | public readonly TextTokenType Type;
180 |
181 | public TextToken(TextTokenType type, string text)
182 | {
183 | Type = type;
184 | Text = text;
185 | }
186 | }
187 |
188 | private enum TextTokenType
189 | {
190 | At, Hash, Text
191 | }
192 |
193 | private struct ConsoleWriteOperation
194 | {
195 | public readonly string Text;
196 | public readonly ConsoleColor ForegroundColor;
197 | public readonly ConsoleColor BackgroundColor;
198 |
199 | public ConsoleWriteOperation(string text, ConsoleColor foregroundColor, ConsoleColor backgroundColor)
200 | {
201 | Text = text;
202 | ForegroundColor = foregroundColor;
203 | BackgroundColor = backgroundColor;
204 | }
205 | }
206 | }
207 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Exceptions/DownloadQuotaReachedException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic.Exceptions
4 | {
5 | public class DownloadQuotaReachedException : Exception
6 | {
7 | }
8 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Exceptions/NestedArchiveNotSupportedException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic.Exceptions
4 | {
5 | public class NestedArchiveNotSupportedException : Exception
6 | {
7 | public NestedArchiveNotSupportedException(string filename)
8 | : base($"Downloaded archive, '{filename}' @red@contain another archive within it and cannot properly be extracted. Archive kept for manual labor.")
9 | {
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Exceptions/RequestQuotaReachedException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic.Exceptions
4 | {
5 | public class RequestQuotaReachedException : Exception
6 | {
7 | }
8 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Exceptions/SubtitleNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic.Exceptions
4 | {
5 | public class SubtitleNotFoundException : Exception
6 | {
7 | }
8 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace SubSyncLib.Logic.Extensions
5 | {
6 | public static class EnumerableExtensions
7 | {
8 | public static void ForEach(this IEnumerable enumerable, Action body)
9 | {
10 | foreach (var item in enumerable)
11 | {
12 | body(item);
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/FallbackSubtitleProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Threading.Tasks;
4 |
5 | namespace SubSyncLib.Logic
6 | {
7 | public class FallbackSubtitleProvider : ISubtitleProvider, IDisposable
8 | {
9 | private readonly ConcurrentDictionary providerCache = new ConcurrentDictionary();
10 | private readonly IVideoSyncList syncList;
11 | private readonly ISubtitleProvider[] _providers;
12 | private readonly int MaxRetryCount = 3;
13 | private bool disposed;
14 |
15 | public FallbackSubtitleProvider(IVideoSyncList syncList, params ISubtitleProvider[] providers)
16 | {
17 | this.syncList = syncList;
18 | _providers = providers;
19 | MaxRetryCount = Math.Max(MaxRetryCount, _providers.Length);
20 | }
21 |
22 | public async Task GetAsync(VideoFile video)
23 | {
24 | providerCache.TryGetValue(video.Name, out var index);
25 | try
26 | {
27 | var result = await _providers[index].GetAsync(video);
28 | providerCache.TryRemove(video.Name, out _);
29 | syncList.Add(video);
30 | return result;
31 | }
32 | catch (Exception exc)
33 | {
34 | providerCache[video.Name] = index + 1;
35 | if (index + 1 > MaxRetryCount || index + 1 >= _providers.Length)
36 | {
37 | providerCache.TryRemove(video.Name, out _);
38 | throw exc;
39 | }
40 |
41 | await Task.Delay(1000);
42 | return await GetAsync(video);
43 | }
44 | }
45 |
46 | public void Dispose()
47 | {
48 | if (disposed)
49 | {
50 | return;
51 | }
52 |
53 | disposed = true;
54 | foreach (var provider in _providers)
55 | {
56 | try
57 | {
58 | if (provider is IDisposable disposable)
59 | {
60 | disposable.Dispose();
61 | }
62 | }
63 | catch
64 | {
65 | // ignored
66 | }
67 | }
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/FileBasedCredentialsProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace SubSyncLib.Logic
5 | {
6 | public class FileBasedCredentialsProvider : IAuthCredentialProvider
7 | {
8 | private readonly string filename;
9 | private readonly ILogger logger;
10 | private bool synchronizedWithFile;
11 | private string username;
12 | private string password;
13 |
14 | public FileBasedCredentialsProvider(string filename, ILogger logger)
15 | {
16 | this.filename = filename;
17 | this.logger = logger;
18 | }
19 |
20 | private void ReadFile()
21 | {
22 | var appPath = AppDomain.CurrentDomain.BaseDirectory;
23 | var filePath = Path.Combine(appPath, filename);
24 | if (!System.IO.File.Exists(filePath))
25 | {
26 | return;
27 | }
28 | // storing the user/pass is generally a very bad idea as you could find the values by looking in the application memory.
29 | // 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
30 | try
31 | {
32 | var lines = System.IO.File.ReadAllLines(filename);
33 | foreach (var line in lines)
34 | {
35 | var data = line.Split('=');
36 | if (data[0].Equals("username", StringComparison.CurrentCultureIgnoreCase))
37 | {
38 | username = data[1];
39 | }
40 | else if (data[0].Equals("password", StringComparison.CurrentCultureIgnoreCase))
41 | {
42 | password = data[1];
43 | }
44 | }
45 | synchronizedWithFile = true;
46 | }
47 | catch (Exception exc)
48 | {
49 | logger.WriteLine("@yel@Unable to read opensubtitles.auth! username and password will be left blank!");
50 | }
51 |
52 | }
53 |
54 | public AuthCredentials Get()
55 | {
56 | if (!synchronizedWithFile)
57 | {
58 | // in case it previously failed or file was added after the application started.
59 | ReadFile();
60 | }
61 |
62 | return new AuthCredentials(username, password);
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/FilenameDiff.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using SubSyncLib.Logic.Extensions;
6 |
7 | namespace SubSyncLib.Logic
8 | {
9 | public static class FilenameDiff
10 | {
11 | public static int IndexOfBestMatch(string needle, string[] haystack)
12 | {
13 | var index = 0;
14 | var scored = new Dictionary();
15 | var input = Path.GetFileNameWithoutExtension(needle);
16 |
17 | foreach (var item in haystack)
18 | {
19 | if (item.Equals(input, StringComparison.OrdinalIgnoreCase))
20 | {
21 | return index; // direct match
22 | }
23 | scored[index++] = GetDiffScore(input, item);
24 | }
25 |
26 | // score: the lower, the better.
27 | if (scored.Count > 0)
28 | {
29 | return scored.OrderBy(x => x.Value).First().Key;
30 | }
31 |
32 | return -1;
33 | }
34 |
35 | public static T FindBestMatch(string needle, T[] haystack, Func stringComparison)
36 | {
37 | var index = IndexOfBestMatch(needle, haystack.Select(stringComparison).ToArray());
38 | return index != -1 ? haystack[index] : default(T);
39 | }
40 |
41 | public static double GetDiffScore(string a, string b)
42 | {
43 | // first iteration is to take all distinct values from a and b
44 | // then compare the actual content and score it.
45 | // second iteration takes words into consideration
46 | var c1 = a.ToLower().ToCharArray();
47 | var c2 = b.ToLower().ToCharArray();
48 |
49 | var diff = new HashSet(c1);
50 | diff.SymmetricExceptWith(c2);
51 |
52 | var changes = diff.ToArray(); // c1.Intersect(c2).ToArray();
53 | var score = 0.0;
54 | // different chars have different scoring
55 | // letters are 1.0
56 | // numbers are 0.75
57 | // brackets and paranthesis are 0.5
58 | // spaces are 0.1
59 | foreach (var change in changes)
60 | {
61 | if (change == '[' || change == ']' || change == '(' || change == ')') score += 0.5;
62 | else if (char.IsDigit(change)) score += 0.75;
63 | else if (change == ' ') score += 0.1;
64 | else score += 1.0;
65 | }
66 |
67 | var l0 = new HashSet();
68 | a.ToLower().Split(new[] { '.', ' ' }, StringSplitOptions.RemoveEmptyEntries).ForEach(x => l0.Add(x));
69 | if (l0.Count > 1) l0.Remove(a.ToLower());
70 |
71 | var l1 = new HashSet();
72 | b.ToLower().Split(new[] { '.', ' ' }, StringSplitOptions.RemoveEmptyEntries).ForEach(x => l1.Add(x));
73 | if (l1.Count > 1) l1.Remove(b.ToLower());
74 |
75 | var l2 = new HashSet();
76 | l2.UnionWith(l0);
77 | l2.UnionWith(l1);
78 |
79 | for (var i = 0; i < l2.Count; i++)
80 | {
81 | if (!l0.Contains(l2.ElementAt(i))) score++;
82 | if (!l1.Contains(l2.ElementAt(i))) score++;
83 | }
84 |
85 | return score;
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IAuthCredentialProvider.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public interface IAuthCredentialProvider
4 | {
5 | AuthCredentials Get();
6 | }
7 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IFileSystemWatcher.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public interface IFileSystemWatcher
4 | {
5 | void Start();
6 | void Stop();
7 | }
8 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/ILogger.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public interface ILogger
4 | {
5 | void Write(string message);
6 | void WriteLine(string message);
7 | void Debug(string message);
8 | void Error(string errorMessage);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IStatusReporter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic
4 | {
5 |
6 | public interface IStatusResultReporter : IStatusReporter
7 | {
8 | event EventHandler OnReportFinished;
9 | }
10 |
11 | public interface IStatusReporter : IStatusReporter
12 | {
13 | void Report(TReportData data);
14 | }
15 |
16 | public interface IStatusReporter
17 | {
18 | void FinishReport();
19 | }
20 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/ISubtitleProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace SubSyncLib.Logic
4 | {
5 | public interface ISubtitleProvider
6 | {
7 | Task GetAsync(VideoFile video);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IVideoIgnoreFilter.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public interface IVideoIgnoreFilter
4 | {
5 | bool Match(string filepath);
6 | bool Match(VideoFile video);
7 | }
8 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IVideoSyncList.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public interface IVideoSyncList
4 | {
5 | bool Contains(VideoFile video);
6 | void Add(VideoFile video);
7 | void Save();
8 | }
9 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IWorker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace SubSyncLib.Logic
5 | {
6 | public interface IWorker : IDisposable
7 | {
8 | Task SyncAsync();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IWorkerProvider.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public interface IWorkerProvider
4 | {
5 | IWorker GetWorker(IWorkerQueue queue, VideoFile video, int tryCount = 0);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/IWorkerQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic
4 | {
5 | public interface IWorkerQueue : IDisposable
6 | {
7 | bool Enqueue(VideoFile video);
8 | void Start();
9 | void Stop();
10 | void Reset();
11 | int Count { get; }
12 | int Active { get; }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/QueueCompletedEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic
4 | {
5 | public class QueueCompletedEventArgs : EventArgs
6 | {
7 | public QueueCompletedEventArgs(int total, int succeeded, int failed)
8 | {
9 | Total = total;
10 | Succeeded = succeeded;
11 | Failed = failed;
12 | }
13 |
14 | public int Total { get; }
15 | public int Succeeded { get; }
16 | public int Failed { get; }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/QueueProcessReporter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 |
5 | namespace SubSyncLib.Logic
6 | {
7 | public class QueueProcessReporter : IStatusReporter, IStatusResultReporter
8 | {
9 | private readonly List failed = new List();
10 | private readonly object reportMutex = new object();
11 | private int total = 0;
12 | private int succeeded = 0;
13 | private int changes = 0;
14 |
15 | public event EventHandler OnReportFinished;
16 |
17 | public void Report(WorkerStatus data)
18 | {
19 | lock (reportMutex)
20 | {
21 | ++total;
22 | if (data.Succeeded)
23 | {
24 | ++succeeded;
25 | }
26 | else
27 | {
28 | failed.Add(data.Target);
29 | }
30 | }
31 | Interlocked.Increment(ref changes);
32 | }
33 |
34 | public void FinishReport()
35 | {
36 | var changeCount = Interlocked.Exchange(ref changes, 0);
37 | if (changeCount == 0)
38 | {
39 | return;
40 | }
41 |
42 | lock (reportMutex)
43 | {
44 | OnReportFinished?.Invoke(this, new QueueProcessResult(total, succeeded, failed.ToArray()));
45 | total = 0;
46 | succeeded = 0;
47 | failed.Clear();
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/QueueProcessResult.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public struct QueueProcessResult
4 | {
5 | public readonly int Total;
6 | public readonly int Succeeded;
7 | public readonly VideoFile[] Failed;
8 |
9 | public QueueProcessResult(int total, int succeeded, VideoFile[] failed)
10 | {
11 | Total = total;
12 | Succeeded = succeeded;
13 | Failed = failed;
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/SubtitleLanguage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace SubSyncLib.Logic
6 | {
7 | public class SubtitleLanguage
8 | {
9 | public string LanguageId { get; }
10 | public string Iso639 { get; }
11 | public string Name { get; }
12 |
13 | public SubtitleLanguage(string languageId, string iso639, string name)
14 | {
15 | LanguageId = languageId;
16 | Iso639 = iso639;
17 | Name = name;
18 | }
19 |
20 | public static SubtitleLanguage Find(string nameIsoOrId)
21 | {
22 | var possibleMatches = new List();
23 |
24 | foreach (var item in All)
25 | {
26 | if (item.LanguageId.Equals(nameIsoOrId, StringComparison.OrdinalIgnoreCase))
27 | {
28 | return item;
29 | }
30 |
31 | if (!string.IsNullOrEmpty(item.Iso639) && item.Iso639.Equals(nameIsoOrId, StringComparison.OrdinalIgnoreCase))
32 | {
33 | return item;
34 | }
35 |
36 | if (item.Name.IndexOf(nameIsoOrId, StringComparison.OrdinalIgnoreCase) >= 0)
37 | {
38 | if (item.Name.Equals(nameIsoOrId, StringComparison.OrdinalIgnoreCase))
39 | {
40 | return item;
41 | }
42 |
43 | possibleMatches.Add(item);
44 | }
45 | }
46 |
47 | if (possibleMatches.Count == 1)
48 | {
49 | return possibleMatches[0];
50 | }
51 |
52 | return possibleMatches
53 | .OrderByDescending(x => x.LanguageId)
54 | .FirstOrDefault();
55 | }
56 |
57 | public static SubtitleLanguage[] All = {
58 | new SubtitleLanguage("aar","aa","Afar, afar"),
59 | new SubtitleLanguage("abk","ab","Abkhazian"),
60 | new SubtitleLanguage("ace","","Achinese"),
61 | new SubtitleLanguage("ach","","Acoli"),
62 | new SubtitleLanguage("ada","","Adangme"),
63 | new SubtitleLanguage("ady","","adyghé"),
64 | new SubtitleLanguage("afa","","Afro-Asiatic (Other)"),
65 | new SubtitleLanguage("afh","","Afrihili"),
66 | new SubtitleLanguage("afr","af","Afrikaans"),
67 | new SubtitleLanguage("ain","","Ainu"),
68 | new SubtitleLanguage("aka","ak","Akan"),
69 | new SubtitleLanguage("akk","","Akkadian"),
70 | new SubtitleLanguage("alb","sq","Albanian"),
71 | new SubtitleLanguage("ale","","Aleut"),
72 | new SubtitleLanguage("alg","","Algonquian languages"),
73 | new SubtitleLanguage("alt","","Southern Altai"),
74 | new SubtitleLanguage("amh","am","Amharic"),
75 | new SubtitleLanguage("ang","","English, Old (ca.450-1100)"),
76 | new SubtitleLanguage("apa","","Apache languages"),
77 | new SubtitleLanguage("ara","ar","Arabic"),
78 | new SubtitleLanguage("arc","","Aramaic"),
79 | new SubtitleLanguage("arg","an","Aragonese"),
80 | new SubtitleLanguage("arm","hy","Armenian"),
81 | new SubtitleLanguage("arn","","Araucanian"),
82 | new SubtitleLanguage("arp","","Arapaho"),
83 | new SubtitleLanguage("art","","Artificial (Other)"),
84 | new SubtitleLanguage("arw","","Arawak"),
85 | new SubtitleLanguage("asm","as","Assamese"),
86 | new SubtitleLanguage("ast","at","Asturian"),
87 | new SubtitleLanguage("ath","","Athapascan languages"),
88 | new SubtitleLanguage("aus","","Australian languages"),
89 | new SubtitleLanguage("ava","av","Avaric"),
90 | new SubtitleLanguage("ave","ae","Avestan"),
91 | new SubtitleLanguage("awa","","Awadhi"),
92 | new SubtitleLanguage("aym","ay","Aymara"),
93 | new SubtitleLanguage("aze","az","Azerbaijani"),
94 | new SubtitleLanguage("bad","","Banda"),
95 | new SubtitleLanguage("bai","","Bamileke languages"),
96 | new SubtitleLanguage("bak","ba","Bashkir"),
97 | new SubtitleLanguage("bal","","Baluchi"),
98 | new SubtitleLanguage("bam","bm","Bambara"),
99 | new SubtitleLanguage("ban","","Balinese"),
100 | new SubtitleLanguage("baq","eu","Basque"),
101 | new SubtitleLanguage("bas","","Basa"),
102 | new SubtitleLanguage("bat","","Baltic (Other)"),
103 | new SubtitleLanguage("bej","","Beja"),
104 | new SubtitleLanguage("bel","be","Belarusian"),
105 | new SubtitleLanguage("bem","","Bemba"),
106 | new SubtitleLanguage("ben","bn","Bengali"),
107 | new SubtitleLanguage("ber","","Berber (Other)"),
108 | new SubtitleLanguage("bho","","Bhojpuri"),
109 | new SubtitleLanguage("bih","bh","Bihari"),
110 | new SubtitleLanguage("bik","","Bikol"),
111 | new SubtitleLanguage("bin","","Bini"),
112 | new SubtitleLanguage("bis","bi","Bislama"),
113 | new SubtitleLanguage("bla","","Siksika"),
114 | new SubtitleLanguage("bnt","","Bantu (Other)"),
115 | new SubtitleLanguage("bos","bs","Bosnian"),
116 | new SubtitleLanguage("bra","","Braj"),
117 | new SubtitleLanguage("bre","br","Breton"),
118 | new SubtitleLanguage("btk","","Batak (Indonesia)"),
119 | new SubtitleLanguage("bua","","Buriat"),
120 | new SubtitleLanguage("bug","","Buginese"),
121 | new SubtitleLanguage("bul","bg","Bulgarian"),
122 | new SubtitleLanguage("bur","my","Burmese"),
123 | new SubtitleLanguage("byn","","Blin"),
124 | new SubtitleLanguage("cad","","Caddo"),
125 | new SubtitleLanguage("cai","","Central American Indian (Other)"),
126 | new SubtitleLanguage("car","","Carib"),
127 | new SubtitleLanguage("cat","ca","Catalan"),
128 | new SubtitleLanguage("cau","","Caucasian (Other)"),
129 | new SubtitleLanguage("ceb","","Cebuano"),
130 | new SubtitleLanguage("cel","","Celtic (Other)"),
131 | new SubtitleLanguage("cha","ch","Chamorro"),
132 | new SubtitleLanguage("chb","","Chibcha"),
133 | new SubtitleLanguage("che","ce","Chechen"),
134 | new SubtitleLanguage("chg","","Chagatai"),
135 | new SubtitleLanguage("chi","zh","Chinese (simplified)"),
136 | new SubtitleLanguage("chk","","Chuukese"),
137 | new SubtitleLanguage("chm","","Mari"),
138 | new SubtitleLanguage("chn","","Chinook jargon"),
139 | new SubtitleLanguage("cho","","Choctaw"),
140 | new SubtitleLanguage("chp","","Chipewyan"),
141 | new SubtitleLanguage("chr","","Cherokee"),
142 | new SubtitleLanguage("chu","cu","Church Slavic"),
143 | new SubtitleLanguage("chv","cv","Chuvash"),
144 | new SubtitleLanguage("chy","","Cheyenne"),
145 | new SubtitleLanguage("cmc","","Chamic languages"),
146 | new SubtitleLanguage("cop","","Coptic"),
147 | new SubtitleLanguage("cor","kw","Cornish"),
148 | new SubtitleLanguage("cos","co","Corsican"),
149 | new SubtitleLanguage("cpe","","Creoles and pidgins, English based (Other)"),
150 | new SubtitleLanguage("cpf","","Creoles and pidgins, French-based (Other)"),
151 | new SubtitleLanguage("cpp","","Creoles and pidgins, Portuguese-based (Other)"),
152 | new SubtitleLanguage("cre","cr","Cree"),
153 | new SubtitleLanguage("crh","","Crimean Tatar"),
154 | new SubtitleLanguage("crp","","Creoles and pidgins (Other)"),
155 | new SubtitleLanguage("csb","","Kashubian"),
156 | new SubtitleLanguage("cus","","Cushitic (Other)' couchitiques, autres langues"),
157 | new SubtitleLanguage("cze","cs","Czech"),
158 | new SubtitleLanguage("dak","","Dakota"),
159 | new SubtitleLanguage("dan","da","Danish"),
160 | new SubtitleLanguage("dar","","Dargwa"),
161 | new SubtitleLanguage("day","","Dayak"),
162 | new SubtitleLanguage("del","","Delaware"),
163 | new SubtitleLanguage("den","","Slave (Athapascan)"),
164 | new SubtitleLanguage("dgr","","Dogrib"),
165 | new SubtitleLanguage("din","","Dinka"),
166 | new SubtitleLanguage("div","dv","Divehi"),
167 | new SubtitleLanguage("doi","","Dogri"),
168 | new SubtitleLanguage("dra","","Dravidian (Other)"),
169 | new SubtitleLanguage("dua","","Duala"),
170 | new SubtitleLanguage("dum","","Dutch, Middle (ca.1050-1350)"),
171 | new SubtitleLanguage("dut","nl","Dutch"),
172 | new SubtitleLanguage("dyu","","Dyula"),
173 | new SubtitleLanguage("dzo","dz","Dzongkha"),
174 | new SubtitleLanguage("efi","","Efik"),
175 | new SubtitleLanguage("egy","","Egyptian (Ancient)"),
176 | new SubtitleLanguage("eka","","Ekajuk"),
177 | new SubtitleLanguage("elx","","Elamite"),
178 | new SubtitleLanguage("eng","en","English"),
179 | new SubtitleLanguage("enm","","English, Middle (1100-1500)"),
180 | new SubtitleLanguage("epo","eo","Esperanto"),
181 | new SubtitleLanguage("est","et","Estonian"),
182 | new SubtitleLanguage("ewe","ee","Ewe"),
183 | new SubtitleLanguage("ewo","","Ewondo"),
184 | new SubtitleLanguage("fan","","Fang"),
185 | new SubtitleLanguage("fao","fo","Faroese"),
186 | new SubtitleLanguage("fat","","Fanti"),
187 | new SubtitleLanguage("fij","fj","Fijian"),
188 | new SubtitleLanguage("fil","","Filipino"),
189 | new SubtitleLanguage("fin","fi","Finnish"),
190 | new SubtitleLanguage("fiu","","Finno-Ugrian (Other)"),
191 | new SubtitleLanguage("fon","","Fon"),
192 | new SubtitleLanguage("fre","fr","French"),
193 | new SubtitleLanguage("frm","","French, Middle (ca.1400-1600)"),
194 | new SubtitleLanguage("fro","","French, Old (842-ca.1400)"),
195 | new SubtitleLanguage("fry","fy","Frisian"),
196 | new SubtitleLanguage("ful","ff","Fulah"),
197 | new SubtitleLanguage("fur","","Friulian"),
198 | new SubtitleLanguage("gaa","","Ga"),
199 | new SubtitleLanguage("gay","","Gayo"),
200 | new SubtitleLanguage("gba","","Gbaya"),
201 | new SubtitleLanguage("gem","","Germanic (Other)"),
202 | new SubtitleLanguage("geo","ka","Georgian"),
203 | new SubtitleLanguage("ger","de","German"),
204 | new SubtitleLanguage("gez","","Geez"),
205 | new SubtitleLanguage("gil","","Gilbertese"),
206 | new SubtitleLanguage("gla","gd","Gaelic"),
207 | new SubtitleLanguage("gle","ga","Irish"),
208 | new SubtitleLanguage("glg","gl","Galician"),
209 | new SubtitleLanguage("glv","gv","Manx"),
210 | new SubtitleLanguage("gmh","","German, Middle High (ca.1050-1500)"),
211 | new SubtitleLanguage("goh","","German, Old High (ca.750-1050)"),
212 | new SubtitleLanguage("gon","","Gondi"),
213 | new SubtitleLanguage("gor","","Gorontalo"),
214 | new SubtitleLanguage("got","","Gothic"),
215 | new SubtitleLanguage("grb","","Grebo"),
216 | new SubtitleLanguage("grc","","Greek, Ancient (to 1453)"),
217 | new SubtitleLanguage("ell","el","Greek"),
218 | new SubtitleLanguage("grn","gn","Guarani"),
219 | new SubtitleLanguage("guj","gu","Gujarati"),
220 | new SubtitleLanguage("gwi","","Gwich´in"),
221 | new SubtitleLanguage("hai","","Haida"),
222 | new SubtitleLanguage("hat","ht","Haitian"),
223 | new SubtitleLanguage("hau","ha","Hausa"),
224 | new SubtitleLanguage("haw","","Hawaiian"),
225 | new SubtitleLanguage("heb","he","Hebrew"),
226 | new SubtitleLanguage("her","hz","Herero"),
227 | new SubtitleLanguage("hil","","Hiligaynon"),
228 | new SubtitleLanguage("him","","Himachali"),
229 | new SubtitleLanguage("hin","hi","Hindi"),
230 | new SubtitleLanguage("hit","","Hittite"),
231 | new SubtitleLanguage("hmn","","Hmong"),
232 | new SubtitleLanguage("hmo","ho","Hiri Motu"),
233 | new SubtitleLanguage("hrv","hr","Croatian"),
234 | new SubtitleLanguage("hun","hu","Hungarian"),
235 | new SubtitleLanguage("hup","","Hupa"),
236 | new SubtitleLanguage("iba","","Iban"),
237 | new SubtitleLanguage("ibo","ig","Igbo"),
238 | new SubtitleLanguage("ice","is","Icelandic"),
239 | new SubtitleLanguage("ido","io","Ido"),
240 | new SubtitleLanguage("iii","ii","Sichuan Yi"),
241 | new SubtitleLanguage("ijo","","Ijo"),
242 | new SubtitleLanguage("iku","iu","Inuktitut"),
243 | new SubtitleLanguage("ile","ie","Interlingue"),
244 | new SubtitleLanguage("ilo","","Iloko"),
245 | new SubtitleLanguage("ina","ia","Interlingua (International Auxiliary Language Asso"),
246 | new SubtitleLanguage("inc","","Indic (Other)"),
247 | new SubtitleLanguage("ind","id","Indonesian"),
248 | new SubtitleLanguage("ine","","Indo-European (Other)"),
249 | new SubtitleLanguage("inh","","Ingush"),
250 | new SubtitleLanguage("ipk","ik","Inupiaq"),
251 | new SubtitleLanguage("ira","","Iranian (Other)"),
252 | new SubtitleLanguage("iro","","Iroquoian languages"),
253 | new SubtitleLanguage("ita","it","Italian"),
254 | new SubtitleLanguage("jav","jv","Javanese"),
255 | new SubtitleLanguage("jpn","ja","Japanese"),
256 | new SubtitleLanguage("jpr","","Judeo-Persian"),
257 | new SubtitleLanguage("jrb","","Judeo-Arabic"),
258 | new SubtitleLanguage("kaa","","Kara-Kalpak"),
259 | new SubtitleLanguage("kab","","Kabyle"),
260 | new SubtitleLanguage("kac","","Kachin"),
261 | new SubtitleLanguage("kal","kl","Kalaallisut"),
262 | new SubtitleLanguage("kam","","Kamba"),
263 | new SubtitleLanguage("kan","kn","Kannada"),
264 | new SubtitleLanguage("kar","","Karen"),
265 | new SubtitleLanguage("kas","ks","Kashmiri"),
266 | new SubtitleLanguage("kau","kr","Kanuri"),
267 | new SubtitleLanguage("kaw","","Kawi"),
268 | new SubtitleLanguage("kaz","kk","Kazakh"),
269 | new SubtitleLanguage("kbd","","Kabardian"),
270 | new SubtitleLanguage("kha","","Khasi"),
271 | new SubtitleLanguage("khi","","Khoisan (Other)"),
272 | new SubtitleLanguage("khm","km","Khmer"),
273 | new SubtitleLanguage("kho","","Khotanese"),
274 | new SubtitleLanguage("kik","ki","Kikuyu"),
275 | new SubtitleLanguage("kin","rw","Kinyarwanda"),
276 | new SubtitleLanguage("kir","ky","Kirghiz"),
277 | new SubtitleLanguage("kmb","","Kimbundu"),
278 | new SubtitleLanguage("kok","","Konkani"),
279 | new SubtitleLanguage("kom","kv","Komi"),
280 | new SubtitleLanguage("kon","kg","Kongo"),
281 | new SubtitleLanguage("kor","ko","Korean"),
282 | new SubtitleLanguage("kos","","Kosraean"),
283 | new SubtitleLanguage("kpe","","Kpelle"),
284 | new SubtitleLanguage("krc","","Karachay-Balkar"),
285 | new SubtitleLanguage("kro","","Kru"),
286 | new SubtitleLanguage("kru","","Kurukh"),
287 | new SubtitleLanguage("kua","kj","Kuanyama"),
288 | new SubtitleLanguage("kum","","Kumyk"),
289 | new SubtitleLanguage("kur","ku","Kurdish"),
290 | new SubtitleLanguage("kut","","Kutenai"),
291 | new SubtitleLanguage("lad","","Ladino"),
292 | new SubtitleLanguage("lah","","Lahnda"),
293 | new SubtitleLanguage("lam","","Lamba"),
294 | new SubtitleLanguage("lao","lo","Lao"),
295 | new SubtitleLanguage("lat","la","Latin"),
296 | new SubtitleLanguage("lav","lv","Latvian"),
297 | new SubtitleLanguage("lez","","Lezghian"),
298 | new SubtitleLanguage("lim","li","Limburgan"),
299 | new SubtitleLanguage("lin","ln","Lingala"),
300 | new SubtitleLanguage("lit","lt","Lithuanian"),
301 | new SubtitleLanguage("lol","","Mongo"),
302 | new SubtitleLanguage("loz","","Lozi"),
303 | new SubtitleLanguage("ltz","lb","Luxembourgish"),
304 | new SubtitleLanguage("lua","","Luba-Lulua"),
305 | new SubtitleLanguage("lub","lu","Luba-Katanga"),
306 | new SubtitleLanguage("lug","lg","Ganda"),
307 | new SubtitleLanguage("lui","","Luiseno"),
308 | new SubtitleLanguage("lun","","Lunda"),
309 | new SubtitleLanguage("luo","","Luo (Kenya and Tanzania)"),
310 | new SubtitleLanguage("lus","","lushai"),
311 | new SubtitleLanguage("mac","mk","Macedonian"),
312 | new SubtitleLanguage("mad","","Madurese"),
313 | new SubtitleLanguage("mag","","Magahi"),
314 | new SubtitleLanguage("mah","mh","Marshallese"),
315 | new SubtitleLanguage("mai","","Maithili"),
316 | new SubtitleLanguage("mak","","Makasar"),
317 | new SubtitleLanguage("mal","ml","Malayalam"),
318 | new SubtitleLanguage("man","","Mandingo"),
319 | new SubtitleLanguage("mao","mi","Maori"),
320 | new SubtitleLanguage("map","","Austronesian (Other)"),
321 | new SubtitleLanguage("mar","mr","Marathi"),
322 | new SubtitleLanguage("mas","","Masai"),
323 | new SubtitleLanguage("may","ms","Malay"),
324 | new SubtitleLanguage("mdf","","Moksha"),
325 | new SubtitleLanguage("mdr","","Mandar"),
326 | new SubtitleLanguage("men","","Mende"),
327 | new SubtitleLanguage("mga","","Irish, Middle (900-1200)"),
328 | new SubtitleLanguage("mic","","Mi'kmaq"),
329 | new SubtitleLanguage("min","","Minangkabau"),
330 | new SubtitleLanguage("mis","","Miscellaneous languages"),
331 | new SubtitleLanguage("mkh","","Mon-Khmer (Other)"),
332 | new SubtitleLanguage("mlg","mg","Malagasy"),
333 | new SubtitleLanguage("mlt","mt","Maltese"),
334 | new SubtitleLanguage("mnc","","Manchu"),
335 | new SubtitleLanguage("mni","ma","Manipuri"),
336 | new SubtitleLanguage("mno","","Manobo languages"),
337 | new SubtitleLanguage("moh","","Mohawk"),
338 | new SubtitleLanguage("mol","mo","Moldavian"),
339 | new SubtitleLanguage("mon","mn","Mongolian"),
340 | new SubtitleLanguage("mos","","Mossi"),
341 | new SubtitleLanguage("mwl","","Mirandese"),
342 | new SubtitleLanguage("mul","","Multiple languages"),
343 | new SubtitleLanguage("mun","","Munda languages"),
344 | new SubtitleLanguage("mus","","Creek"),
345 | new SubtitleLanguage("mwr","","Marwari"),
346 | new SubtitleLanguage("myn","","Mayan languages"),
347 | new SubtitleLanguage("myv","","Erzya"),
348 | new SubtitleLanguage("nah","","Nahuatl"),
349 | new SubtitleLanguage("nai","","North American Indian"),
350 | new SubtitleLanguage("nap","","Neapolitan"),
351 | new SubtitleLanguage("nau","na","Nauru"),
352 | new SubtitleLanguage("nav","nv","Navajo"),
353 | new SubtitleLanguage("nbl","nr","Ndebele, South"),
354 | new SubtitleLanguage("nde","nd","Ndebele, North"),
355 | new SubtitleLanguage("ndo","ng","Ndonga"),
356 | new SubtitleLanguage("nds","","Low German"),
357 | new SubtitleLanguage("nep","ne","Nepali"),
358 | new SubtitleLanguage("new","","Nepal Bhasa"),
359 | new SubtitleLanguage("nia","","Nias"),
360 | new SubtitleLanguage("nic","","Niger-Kordofanian (Other)"),
361 | new SubtitleLanguage("niu","","Niuean"),
362 | new SubtitleLanguage("nno","nn","Norwegian Nynorsk"),
363 | new SubtitleLanguage("nob","nb","Norwegian Bokmal"),
364 | new SubtitleLanguage("nog","","Nogai"),
365 | new SubtitleLanguage("non","","Norse, Old"),
366 | new SubtitleLanguage("nor","no","Norwegian"),
367 | new SubtitleLanguage("nso","","Northern Sotho"),
368 | new SubtitleLanguage("nub","","Nubian languages"),
369 | new SubtitleLanguage("nwc","","Classical Newari"),
370 | new SubtitleLanguage("nya","ny","Chichewa"),
371 | new SubtitleLanguage("nym","","Nyamwezi"),
372 | new SubtitleLanguage("nyn","","Nyankole"),
373 | new SubtitleLanguage("nyo","","Nyoro"),
374 | new SubtitleLanguage("nzi","","Nzima"),
375 | new SubtitleLanguage("oci","oc","Occitan"),
376 | new SubtitleLanguage("oji","oj","Ojibwa"),
377 | new SubtitleLanguage("ori","or","Oriya"),
378 | new SubtitleLanguage("orm","om","Oromo"),
379 | new SubtitleLanguage("osa","","Osage"),
380 | new SubtitleLanguage("oss","os","Ossetian"),
381 | new SubtitleLanguage("ota","","Turkish, Ottoman (1500-1928)"),
382 | new SubtitleLanguage("oto","","Otomian languages"),
383 | new SubtitleLanguage("paa","","Papuan (Other)"),
384 | new SubtitleLanguage("pag","","Pangasinan"),
385 | new SubtitleLanguage("pal","","Pahlavi"),
386 | new SubtitleLanguage("pam","","Pampanga"),
387 | new SubtitleLanguage("pan","pa","Panjabi"),
388 | new SubtitleLanguage("pap","","Papiamento"),
389 | new SubtitleLanguage("pau","","Palauan"),
390 | new SubtitleLanguage("peo","","Persian, Old (ca.600-400 B.C.)"),
391 | new SubtitleLanguage("per","fa","Persian"),
392 | new SubtitleLanguage("phi","","Philippine (Other)"),
393 | new SubtitleLanguage("phn","","Phoenician"),
394 | new SubtitleLanguage("pli","pi","Pali"),
395 | new SubtitleLanguage("pol","pl","Polish"),
396 | new SubtitleLanguage("pon","","Pohnpeian"),
397 | new SubtitleLanguage("por","pt","Portuguese"),
398 | new SubtitleLanguage("pra","","Prakrit languages"),
399 | new SubtitleLanguage("pro","","Provençal, Old (to 1500)"),
400 | new SubtitleLanguage("pus","ps","Pushto"),
401 | new SubtitleLanguage("que","qu","Quechua"),
402 | new SubtitleLanguage("raj","","Rajasthani"),
403 | new SubtitleLanguage("rap","","Rapanui"),
404 | new SubtitleLanguage("rar","","Rarotongan"),
405 | new SubtitleLanguage("roa","","Romance (Other)"),
406 | new SubtitleLanguage("roh","rm","Raeto-Romance"),
407 | new SubtitleLanguage("rom","","Romany"),
408 | new SubtitleLanguage("run","rn","Rundi"),
409 | new SubtitleLanguage("rup","","Aromanian"),
410 | new SubtitleLanguage("rus","ru","Russian"),
411 | new SubtitleLanguage("sad","","Sandawe"),
412 | new SubtitleLanguage("sag","sg","Sango"),
413 | new SubtitleLanguage("sah","","Yakut"),
414 | new SubtitleLanguage("sai","","South American Indian (Other)"),
415 | new SubtitleLanguage("sal","","Salishan languages"),
416 | new SubtitleLanguage("sam","","Samaritan Aramaic"),
417 | new SubtitleLanguage("san","sa","Sanskrit"),
418 | new SubtitleLanguage("sas","","Sasak"),
419 | new SubtitleLanguage("sat","","Santali"),
420 | new SubtitleLanguage("scc","sr","Serbian"),
421 | new SubtitleLanguage("scn","","Sicilian"),
422 | new SubtitleLanguage("sco","","Scots"),
423 | new SubtitleLanguage("sel","","Selkup"),
424 | new SubtitleLanguage("sem","","Semitic (Other)"),
425 | new SubtitleLanguage("sga","","Irish, Old (to 900)"),
426 | new SubtitleLanguage("sgn","","Sign Languages"),
427 | new SubtitleLanguage("shn","","Shan"),
428 | new SubtitleLanguage("sid","","Sidamo"),
429 | new SubtitleLanguage("sin","si","Sinhalese"),
430 | new SubtitleLanguage("sio","","Siouan languages"),
431 | new SubtitleLanguage("sit","","Sino-Tibetan (Other)"),
432 | new SubtitleLanguage("sla","","Slavic (Other)"),
433 | new SubtitleLanguage("slo","sk","Slovak"),
434 | new SubtitleLanguage("slv","sl","Slovenian"),
435 | new SubtitleLanguage("sma","","Southern Sami"),
436 | new SubtitleLanguage("sme","se","Northern Sami"),
437 | new SubtitleLanguage("smi","","Sami languages (Other)"),
438 | new SubtitleLanguage("smj","","Lule Sami"),
439 | new SubtitleLanguage("smn","","Inari Sami"),
440 | new SubtitleLanguage("smo","sm","Samoan"),
441 | new SubtitleLanguage("sms","","Skolt Sami"),
442 | new SubtitleLanguage("sna","sn","Shona"),
443 | new SubtitleLanguage("snd","sd","Sindhi"),
444 | new SubtitleLanguage("snk","","Soninke"),
445 | new SubtitleLanguage("sog","","Sogdian"),
446 | new SubtitleLanguage("som","so","Somali"),
447 | new SubtitleLanguage("son","","Songhai"),
448 | new SubtitleLanguage("sot","st","Sotho, Southern"),
449 | new SubtitleLanguage("spa","es","Spanish"),
450 | new SubtitleLanguage("srd","sc","Sardinian"),
451 | new SubtitleLanguage("srr","","Serer"),
452 | new SubtitleLanguage("ssa","","Nilo-Saharan (Other)"),
453 | new SubtitleLanguage("ssw","ss","Swati"),
454 | new SubtitleLanguage("suk","","Sukuma"),
455 | new SubtitleLanguage("sun","su","Sundanese"),
456 | new SubtitleLanguage("sus","","Susu"),
457 | new SubtitleLanguage("sux","","Sumerian"),
458 | new SubtitleLanguage("swa","sw","Swahili"),
459 | new SubtitleLanguage("swe","sv","Swedish"),
460 | new SubtitleLanguage("syr","sy","Syriac"),
461 | new SubtitleLanguage("tah","ty","Tahitian"),
462 | new SubtitleLanguage("tai","","Tai (Other)"),
463 | new SubtitleLanguage("tam","ta","Tamil"),
464 | new SubtitleLanguage("tat","tt","Tatar"),
465 | new SubtitleLanguage("tel","te","Telugu"),
466 | new SubtitleLanguage("tem","","Timne"),
467 | new SubtitleLanguage("ter","","Tereno"),
468 | new SubtitleLanguage("tet","","Tetum"),
469 | new SubtitleLanguage("tgk","tg","Tajik"),
470 | new SubtitleLanguage("tgl","tl","Tagalog"),
471 | new SubtitleLanguage("tha","th","Thai"),
472 | new SubtitleLanguage("tib","bo","Tibetan"),
473 | new SubtitleLanguage("tig","","Tigre"),
474 | new SubtitleLanguage("tir","ti","Tigrinya"),
475 | new SubtitleLanguage("tiv","","Tiv"),
476 | new SubtitleLanguage("tkl","","Tokelau"),
477 | new SubtitleLanguage("tlh","","Klingon"),
478 | new SubtitleLanguage("tli","","Tlingit"),
479 | new SubtitleLanguage("tmh","","Tamashek"),
480 | new SubtitleLanguage("tog","","Tonga (Nyasa)"),
481 | new SubtitleLanguage("ton","to","Tonga (Tonga Islands)"),
482 | new SubtitleLanguage("tpi","","Tok Pisin"),
483 | new SubtitleLanguage("tsi","","Tsimshian"),
484 | new SubtitleLanguage("tsn","tn","Tswana"),
485 | new SubtitleLanguage("tso","ts","Tsonga"),
486 | new SubtitleLanguage("tuk","tk","Turkmen"),
487 | new SubtitleLanguage("tum","","Tumbuka"),
488 | new SubtitleLanguage("tup","","Tupi languages"),
489 | new SubtitleLanguage("tur","tr","Turkish"),
490 | new SubtitleLanguage("tut","","Altaic (Other)"),
491 | new SubtitleLanguage("tvl","","Tuvalu"),
492 | new SubtitleLanguage("twi","tw","Twi"),
493 | new SubtitleLanguage("tyv","","Tuvinian"),
494 | new SubtitleLanguage("udm","","Udmurt"),
495 | new SubtitleLanguage("uga","","Ugaritic"),
496 | new SubtitleLanguage("uig","ug","Uighur"),
497 | new SubtitleLanguage("ukr","uk","Ukrainian"),
498 | new SubtitleLanguage("umb","","Umbundu"),
499 | new SubtitleLanguage("und","","Undetermined"),
500 | new SubtitleLanguage("urd","ur","Urdu"),
501 | new SubtitleLanguage("uzb","uz","Uzbek"),
502 | new SubtitleLanguage("vai","","Vai"),
503 | new SubtitleLanguage("ven","ve","Venda"),
504 | new SubtitleLanguage("vie","vi","Vietnamese"),
505 | new SubtitleLanguage("vol","vo","Volapük"),
506 | new SubtitleLanguage("vot","","Votic"),
507 | new SubtitleLanguage("wak","","Wakashan languages"),
508 | new SubtitleLanguage("wal","","Walamo"),
509 | new SubtitleLanguage("war","","Waray"),
510 | new SubtitleLanguage("was","","Washo"),
511 | new SubtitleLanguage("wel","cy","Welsh"),
512 | new SubtitleLanguage("wen","","Sorbian languages"),
513 | new SubtitleLanguage("wln","wa","Walloon"),
514 | new SubtitleLanguage("wol","wo","Wolof"),
515 | new SubtitleLanguage("xal","","Kalmyk"),
516 | new SubtitleLanguage("xho","xh","Xhosa"),
517 | new SubtitleLanguage("yao","","Yao"),
518 | new SubtitleLanguage("yap","","Yapese"),
519 | new SubtitleLanguage("yid","yi","Yiddish"),
520 | new SubtitleLanguage("yor","yo","Yoruba"),
521 | new SubtitleLanguage("ypk","","Yupik languages"),
522 | new SubtitleLanguage("zap","","Zapotec"),
523 | new SubtitleLanguage("zen","","Zenaga"),
524 | new SubtitleLanguage("zha","za","Zhuang"),
525 | new SubtitleLanguage("znd","","Zande"),
526 | new SubtitleLanguage("zul","zu","Zulu"),
527 | new SubtitleLanguage("zun","","Zuni"),
528 | new SubtitleLanguage("rum","ro","Romanian"),
529 | new SubtitleLanguage("pob","pb","Portuguese (BR)"),
530 | new SubtitleLanguage("mne","me","Montenegrin"),
531 | new SubtitleLanguage("zht","zt","Chinese (traditional)"),
532 | new SubtitleLanguage("zhe","ze","Chinese bilingual"),
533 | new SubtitleLanguage("pom","pm","Portuguese (MZ)"),
534 | new SubtitleLanguage("ext","ex","Extremadura"),
535 | };
536 | }
537 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/SubtitleProviderBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Runtime.CompilerServices;
7 | using System.Threading.Tasks;
8 |
9 | namespace SubSyncLib.Logic
10 | {
11 | public abstract class SubtitleProviderBase : ISubtitleProvider
12 | {
13 | protected readonly HashSet Languages;
14 |
15 | protected SubtitleProviderBase(HashSet languages)
16 | {
17 | Languages = languages;
18 | }
19 |
20 | protected string UserAgent { get; set; } = "SubSync10";
21 |
22 | protected int RequestRetryLimit { get; set; } = 3;
23 |
24 | protected int RequestTimeout { get; set; } = 10000;
25 |
26 | public abstract Task GetAsync(VideoFile video);
27 |
28 | protected async Task DownloadFileAsync(string url, string outputDirectory, int retryCount = 0)
29 | {
30 | try
31 | {
32 | var filename = "download.zip";
33 | var req = CreateRequest(url);
34 | using (var response = (HttpWebResponse)await req.GetResponseAsync())
35 | {
36 | var contentDisposition = response.Headers.Get("Content-Disposition");
37 | if (!string.IsNullOrEmpty(contentDisposition))
38 | {
39 | var newFileName = contentDisposition.Split('=').LastOrDefault();
40 | if (!string.IsNullOrEmpty(newFileName))
41 | filename = newFileName;
42 | }
43 |
44 | var outputFile = Path.Combine(outputDirectory, filename);
45 | using (var stream = response.GetResponseStream())
46 | {
47 | var file = new FileInfo(outputFile);
48 | using (var output = file.Create())
49 | {
50 | int read = 0;
51 | var buffer = new byte[4096];
52 |
53 | while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) != 0)
54 | {
55 | await output.WriteAsync(buffer, 0, read);
56 | }
57 |
58 | return outputFile;
59 | }
60 | }
61 | }
62 | }
63 | catch (WebException webException)
64 | {
65 | if ((int)webException.Status != 409 && webException.Status != WebExceptionStatus.ProtocolError) // 409: conflict
66 | {
67 | throw new WebException($"Downloading file from url: {url} failed.", webException);
68 | }
69 |
70 | if (retryCount >= RequestRetryLimit)
71 | {
72 | throw new WebException($"Downloading file from url: {url} failed after {retryCount} retries.", webException);
73 | }
74 |
75 | await Task.Delay(1000 * (retryCount + 1));
76 | return await DownloadFileAsync(url, outputDirectory, ++retryCount);
77 | }
78 | }
79 |
80 | protected async Task DownloadStringAsync(string url, int retryCount = 0)
81 | {
82 | try
83 | {
84 | return await GetResponseStringAsync(CreateRequest(url));
85 | }
86 | catch (WebException webException)
87 | {
88 | if ((int)webException.Status != 409 && webException.Status != WebExceptionStatus.ProtocolError) // 409: conflict
89 | {
90 | throw new WebException($"Request to url: {url} failed.", webException);
91 | }
92 |
93 | if (retryCount >= RequestRetryLimit)
94 | {
95 | throw new WebException($"Request to url: {url} failed after {retryCount} retries.", webException);
96 | }
97 |
98 | await Task.Delay(1000 * (retryCount + 1));
99 |
100 | if (webException.Response != null)
101 | {
102 | var responseStream = webException.Response.GetResponseStream();
103 | var errorMessage = "";
104 | if (responseStream != null)
105 | {
106 | using (var sr = new StreamReader(responseStream))
107 | {
108 | errorMessage = sr.ReadToEnd();
109 | if (!string.IsNullOrEmpty(errorMessage))
110 | {
111 | if (errorMessage.ToLower().Contains("too many requests"))
112 | {
113 | throw new WebException($"Request to url: {url} failed. Too many requests");
114 | }
115 | else
116 | {
117 | //errorMessage
118 | throw new WebException($"Request to url: {url} failed. Server returned: {errorMessage}", webException);
119 | }
120 |
121 | }
122 | }
123 | }
124 | }
125 | return await DownloadStringAsync(url, ++retryCount);
126 | }
127 | }
128 |
129 | protected async Task GetResponseStringAsync(HttpWebRequest request)
130 | {
131 | using (var response = await request.GetResponseAsync())
132 | {
133 | return await GetResponseStringAsync(response);
134 | }
135 | }
136 |
137 | protected async Task GetResponseStringAsync(WebResponse response)
138 | {
139 | using (var stream = response.GetResponseStream())
140 | using (var sr = new StreamReader(stream))
141 | {
142 | return await sr.ReadToEndAsync();
143 | }
144 | }
145 |
146 | protected HttpWebRequest CreatePostAsync(string url, string data)
147 | {
148 | if (data == null)
149 | {
150 | throw new ArgumentNullException(nameof(data));
151 | }
152 |
153 | if (data.StartsWith("<"))
154 | {
155 | return CreatePostXmlAsync(url, data);
156 | }
157 |
158 | if (data.StartsWith("[") || data.StartsWith("{"))
159 | {
160 | return CreatePostJsonAsync(url, data);
161 | }
162 |
163 | return CreatePostTextAsync(url, data);
164 | }
165 |
166 | protected HttpWebRequest CreatePostXmlAsync(string url, string data)
167 | {
168 | return CreatePostRequest(url, data, "text/xml");
169 | }
170 |
171 | protected HttpWebRequest CreatePostJsonAsync(string url, string data)
172 | {
173 | return CreatePostRequest(url, data, "application/json");
174 | }
175 |
176 | protected HttpWebRequest CreatePostTextAsync(string url, string data)
177 | {
178 | return CreatePostRequest(url, data, "text/plain");
179 | }
180 |
181 | protected HttpWebRequest CreatePostRequest(string url, string data, string contentType)
182 | {
183 | var req = CreateRequest(url, "POST");
184 | req.ContentType = contentType;
185 | req.Host = url.Split(new[] { "://" }, StringSplitOptions.None)[1].Split('/').FirstOrDefault();
186 | using (var reqStream = req.GetRequestStream())
187 | using (var writer = new StreamWriter(reqStream))
188 | {
189 | writer.Write(data);
190 | }
191 |
192 | return req;
193 | }
194 |
195 | private HttpWebRequest CreateRequest(string url, string method = "GET")
196 | {
197 | var req = WebRequest.CreateHttp(url);
198 | req.Method = method;
199 | req.UserAgent = UserAgent;
200 | req.Timeout = req.ReadWriteTimeout = RequestTimeout;
201 | req.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
202 | req.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip, deflate");
203 | return req;
204 | }
205 |
206 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
207 | protected static string GetUrlFriendlyName(string name)
208 | {
209 | return WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name));
210 | }
211 | }
212 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/SubtitleSynchronizer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.CompilerServices;
6 | using System.Threading;
7 | using SubSyncLib.Logic.Extensions;
8 |
9 | namespace SubSyncLib.Logic
10 | {
11 | public class SubtitleSynchronizer : IFileSystemWatcher, IDisposable
12 | {
13 | private readonly ILogger logger;
14 | private readonly IVideoSyncList syncList;
15 | private readonly IWorkerQueue workerQueue;
16 | private readonly IStatusResultReporter statusReporter;
17 | private readonly IVideoIgnoreFilter videoIgnore;
18 | private readonly SubSyncSettings settings;
19 | private FileSystemWatcher fsWatcher;
20 | private bool disposed;
21 | private int skipped = 0;
22 |
23 | public SubtitleSynchronizer(
24 | ILogger logger,
25 | IVideoSyncList syncList,
26 | IWorkerQueue workerQueue,
27 | IStatusResultReporter statusReporter,
28 | IVideoIgnoreFilter videoIgnore,
29 | SubSyncSettings settings)
30 | {
31 | this.logger = logger;
32 | this.syncList = syncList;
33 | this.workerQueue = workerQueue;
34 | this.statusReporter = statusReporter;
35 | this.videoIgnore = videoIgnore;
36 | this.settings = settings;
37 | }
38 |
39 | public void Dispose()
40 | {
41 | if (disposed) return;
42 | Stop();
43 | fsWatcher?.Dispose();
44 | workerQueue.Dispose();
45 | disposed = true;
46 | }
47 |
48 | public void Start()
49 | {
50 | if (fsWatcher != null) return;
51 | fsWatcher = new FileSystemWatcher(settings.Input, "*.*");
52 | fsWatcher.Error += FsWatcherOnError;
53 | fsWatcher.IncludeSubdirectories = true;
54 | fsWatcher.EnableRaisingEvents = true;
55 | fsWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.DirectoryName |
56 | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite |
57 | NotifyFilters.Size | NotifyFilters.Security;
58 |
59 | fsWatcher.Created += FileCreated;
60 | fsWatcher.Renamed += FileCreated;
61 | statusReporter.OnReportFinished += ResultReport;
62 | workerQueue.Start();
63 | SyncAll();
64 | }
65 |
66 | private void StopAndExit()
67 | {
68 | Stop();
69 | Environment.Exit(0);
70 | }
71 |
72 | public void Stop()
73 | {
74 | if (fsWatcher == null) return;
75 | statusReporter.OnReportFinished -= ResultReport;
76 | workerQueue.Stop();
77 | fsWatcher.Error -= FsWatcherOnError;
78 | fsWatcher.Created -= FileCreated;
79 | fsWatcher.Renamed -= FileCreated;
80 | fsWatcher.Dispose();
81 | fsWatcher = null;
82 | }
83 |
84 | private void ResultReport(object sender, QueueProcessResult result)
85 | {
86 | var skipcount = Interlocked.Exchange(ref skipped, 0);
87 | var total = result.Total;
88 | var failed = result.Failed;
89 | var success = result.Succeeded;
90 |
91 | if (total == 0 && skipcount == 0)
92 | {
93 | return;
94 | }
95 |
96 | logger.WriteLine($"");
97 | logger.WriteLine($" ═════════════════════════════════════════════════════");
98 | logger.WriteLine($"");
99 | logger.WriteLine($" @whi@Synchronization completed with a total of @yel@{total} @whi@video(s) processed.");
100 |
101 | logger.WriteLine($" {skipcount} video(s) was skipped.");
102 | if (success > 0)
103 | {
104 | logger.WriteLine($" @green@{success} @whi@video(s) was successefully was synchronized.");
105 | }
106 | if (failed.Length > 0)
107 | {
108 | logger.WriteLine($" @red@{failed.Length} @whi@video(s) failed to synchronize.");
109 | foreach (var failedItem in failed)
110 | {
111 | logger.WriteLine($" @red@* {failedItem.Name}");
112 | }
113 | }
114 |
115 | syncList.Save();
116 |
117 | if (settings.ExitAfterSync)
118 | {
119 | StopAndExit();
120 | }
121 | }
122 |
123 | public void SyncAll()
124 | {
125 | var directoryInfo = new SystemFilteredDirectoryInfo(settings.Input);
126 | workerQueue.Reset();
127 | settings.VideoExt
128 | .SelectMany(y => directoryInfo.GetFiles($"*{y}", SearchOption.AllDirectories)).Select(x => x.FullName)
129 | .ForEach(Sync);
130 |
131 | if (settings.ExitAfterSync
132 | && workerQueue.Count == 0
133 | && workerQueue.Active == 0)
134 | {
135 | StopAndExit();
136 | }
137 | }
138 |
139 | private void Sync(string fullFilePath)
140 | {
141 | try
142 | {
143 | var video = new VideoFile(fullFilePath);
144 |
145 | if (IsSynchronized(video))
146 | {
147 | Interlocked.Increment(ref skipped);
148 | return;
149 | }
150 |
151 | workerQueue.Enqueue(video);
152 | }
153 | catch (Exception exc)
154 | {
155 | logger.Error($"Unable to sync subtitles for @yellow@{fullFilePath} @red@, reason: {exc.Message}.");
156 | }
157 | }
158 |
159 | private void FsWatcherOnError(object sender, ErrorEventArgs errorEventArgs)
160 | {
161 | logger.Error($"PANIC!! Fatal Media Watcher Error !! {errorEventArgs.GetException().Message}");
162 | }
163 |
164 | private void FileCreated(object sender, FileSystemEventArgs e)
165 | {
166 | if (!settings.VideoExt.Contains(Path.GetExtension(e.FullPath)))
167 | {
168 | return;
169 | }
170 |
171 | Sync(e.FullPath);
172 | }
173 |
174 | private bool IsSynchronized(VideoFile videoFile)
175 | {
176 | if (settings.ResyncAll)
177 | {
178 | // regardless, redownload this subtitle
179 | return false;
180 | }
181 |
182 |
183 | if (BlacklistedFile(videoFile))
184 | {
185 | return true;
186 | }
187 |
188 | if (videoIgnore.Match(videoFile))
189 | {
190 | return true;
191 | }
192 |
193 | if (videoFile.Directory == null)
194 | {
195 | throw new NullReferenceException(nameof(videoFile.Directory));
196 | }
197 |
198 | var hasSubtitleFile = HasSubtitleFile(settings, videoFile);
199 |
200 | // check if in sync list
201 | if (syncList.Contains(videoFile) && hasSubtitleFile)
202 | {
203 | return true;
204 | }
205 |
206 | if (settings.Resync)
207 | {
208 | // normal --resync flag just means we want to redownload any subtitles subsync has not synced.
209 | return false;
210 | }
211 |
212 | return hasSubtitleFile;
213 | }
214 |
215 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
216 | private static bool BlacklistedFile(VideoFile video)
217 | {
218 | // todo: make this configurable. but for now, ignore all sample. files.
219 | return Path.GetFileNameWithoutExtension(video.Name).Equals("sample", StringComparison.OrdinalIgnoreCase);
220 | }
221 |
222 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
223 | private static bool HasSubtitleFile(SubSyncSettings settings, VideoFile videoFile)
224 | {
225 | return settings.SubtitleExt.SelectMany(x =>
226 | videoFile.Directory.GetFiles($"{Path.GetFileNameWithoutExtension(videoFile.Name)}{x}", SearchOption.AllDirectories))
227 | .Any();
228 | }
229 | }
230 |
231 | public class VideoFile
232 | {
233 | private readonly FileInfo fileInfo;
234 |
235 | public VideoFile(string fullFilePath)
236 | {
237 | FilePath = fullFilePath;
238 | fileInfo = new FileInfo(fullFilePath);
239 |
240 | //Hash = Utilities.ComputeMovieHash(fullFilePath);
241 | //HashString = Utilities.ToHexadecimal(Hash);
242 | }
243 |
244 | public string FilePath { get; }
245 | //public byte[] Hash { get; }
246 | //public string HashString { get; }
247 | public string Name => fileInfo.Name;
248 |
249 | public IDirectoryInfo Directory => new SystemFilteredDirectoryInfo(fileInfo.Directory);
250 | }
251 |
252 | public class SystemFilteredDirectoryInfo : IDirectoryInfo
253 | {
254 | private readonly DirectoryInfo directory;
255 |
256 | public SystemFilteredDirectoryInfo(string path)
257 | {
258 | this.directory = new DirectoryInfo(path);
259 | }
260 |
261 | public SystemFilteredDirectoryInfo(DirectoryInfo directory)
262 | {
263 | this.directory = directory;
264 | }
265 |
266 | public string FullName => this.directory.FullName;
267 |
268 | public IEnumerable GetFiles(string pattern, SearchOption searchOptions)
269 | {
270 | return GetFileInfosImpl(directory, pattern, searchOptions);
271 | }
272 |
273 | private IEnumerable GetFileInfosImpl(DirectoryInfo dir, string pattern, SearchOption searchOptions)
274 | {
275 | var files = dir.GetFiles(pattern, SearchOption.TopDirectoryOnly).ToList();
276 | if (searchOptions != SearchOption.AllDirectories)
277 | {
278 | return files;
279 | }
280 |
281 | var allDirs = dir
282 | .GetDirectories("*", SearchOption.TopDirectoryOnly)
283 | .ToList();
284 |
285 | var allFiles = allDirs
286 | .Where(x => !x.Attributes.HasFlag(FileAttributes.System))
287 | .SelectMany(x => GetFileInfosImpl(x, pattern, SearchOption.AllDirectories));
288 | files.AddRange(allFiles);
289 |
290 | return files;
291 | }
292 | }
293 |
294 | public interface IDirectoryInfo
295 | {
296 | string FullName { get; }
297 | IEnumerable GetFiles(string pattern, SearchOption searchOptions);
298 | }
299 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Utilities.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using SharpCompress.Compressors;
5 | using SharpCompress.Compressors.Deflate;
6 |
7 | namespace SubSyncLib.Logic
8 | {
9 | public static class Utilities
10 | {
11 | public static byte[] ComputeMovieHash(string filename)
12 | {
13 | byte[] result;
14 | using (Stream input = File.OpenRead(filename))
15 | {
16 | result = ComputeMovieHash(input);
17 | }
18 | return result;
19 | }
20 |
21 | public static byte[] ComputeMovieHash(Stream input)
22 | {
23 | long lhash, streamsize;
24 | streamsize = input.Length;
25 | lhash = streamsize;
26 |
27 | long i = 0;
28 | byte[] buffer = new byte[sizeof(long)];
29 | while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0))
30 | {
31 | i++;
32 | lhash += BitConverter.ToInt64(buffer, 0);
33 | }
34 |
35 | input.Position = Math.Max(0, streamsize - 65536);
36 | i = 0;
37 | while (i < 65536 / sizeof(long) && (input.Read(buffer, 0, sizeof(long)) > 0))
38 | {
39 | i++;
40 | lhash += BitConverter.ToInt64(buffer, 0);
41 | }
42 | input.Close();
43 | byte[] result = BitConverter.GetBytes(lhash);
44 | Array.Reverse(result);
45 | return result;
46 | }
47 |
48 | public static string ToHexadecimal(byte[] bytes)
49 | {
50 | StringBuilder hexBuilder = new StringBuilder();
51 | for (int i = 0; i < bytes.Length; i++)
52 | {
53 | hexBuilder.Append(bytes[i].ToString("x2"));
54 | }
55 | return hexBuilder.ToString();
56 | }
57 |
58 | public static string DecompressGzipBase64(string base64)
59 | {
60 | var compressedBytes = Convert.FromBase64String(base64);
61 | var data = GzipDecompress(compressedBytes);
62 | return Encoding.UTF8.GetString(data);
63 | }
64 |
65 | public static byte[] GzipDecompress(byte[] gzip)
66 | {
67 | var buffer = new byte[4096];
68 | using (var stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress))
69 | using (var memory = new MemoryStream())
70 | {
71 | var size = 0;
72 | while ((size = stream.Read(buffer, 0, buffer.Length)) != 0)
73 | {
74 | memory.Write(buffer, 0, size);
75 | }
76 | return memory.ToArray();
77 | }
78 | }
79 |
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/VideoFormats/IVideoHeader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic.VideoFormats
4 | {
5 | public interface IVideoHeader
6 | {
7 | bool HasSubtitles { get; }
8 | TimeSpan Length { get; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/VideoFormats/MatroskaVideo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace SubSyncLib.Logic.VideoFormats
6 | {
7 | public interface IVideoHeaderProvider
8 | {
9 | bool IsSupported(VideoFile file);
10 | IVideoHeader Get(VideoFile file);
11 | }
12 |
13 | public class VideoHeaderProvider : IVideoHeaderProvider
14 | {
15 | public bool IsSupported(VideoFile file)
16 | {
17 | // return Get(file) != null;
18 | switch (System.IO.Path.GetExtension(file.Name.ToLower()))
19 | {
20 | case ".mkv": return true;
21 | default: return false;
22 | }
23 | }
24 |
25 | public IVideoHeader Get(VideoFile file)
26 | {
27 | switch (System.IO.Path.GetExtension(file.Name.ToLower()))
28 | {
29 | case ".mkv": return new MatroskaVideoHeader(file);
30 | default: return null;
31 | }
32 | }
33 | }
34 |
35 | public class MatroskaVideoHeader : IVideoHeader
36 | {
37 | private readonly VideoFile file;
38 |
39 | public MatroskaVideoHeader(VideoFile file)
40 | {
41 | this.file = file;
42 |
43 | }
44 |
45 | public bool HasSubtitles { get; }
46 |
47 | public TimeSpan Length { get; }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/VideoIgnoreFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace SubSyncLib.Logic
5 | {
6 | public class VideoIgnoreFilter : IVideoIgnoreFilter
7 | {
8 | private readonly IReadOnlyList filters;
9 |
10 | public VideoIgnoreFilter(IEnumerable filters)
11 | {
12 | this.filters = BuildFilters(filters);
13 | }
14 |
15 | private IReadOnlyList BuildFilters(IEnumerable items)
16 | {
17 | var result = new List();
18 |
19 | foreach (var filter in items)
20 | {
21 | result.Add(BuildFilter(filter));
22 | }
23 |
24 | return result;
25 | }
26 |
27 | private IVideoIgnoreFilter BuildFilter(string filter)
28 | {
29 | filter = filter.Replace('\\', '/');
30 | if (string.IsNullOrEmpty(filter))
31 | {
32 | return new PassthroughFilter();
33 | }
34 |
35 | if (filter.Contains("*"))
36 | {
37 | return new FuzzyFilter(filter);
38 | }
39 |
40 | return new EndsWithFilter(filter);
41 | }
42 |
43 | public bool Match(VideoFile video)
44 | {
45 | return Match(video.FilePath);
46 | }
47 |
48 | public bool Match(string filepath)
49 | {
50 | // return true if filter match
51 | filepath = filepath.Replace('\\', '/');
52 | foreach (var filter in filters)
53 | {
54 | if (filter.Match(filepath))
55 | {
56 | return true;
57 | }
58 | }
59 |
60 | return false;
61 | }
62 | }
63 |
64 | internal class EndsWithFilter : IVideoIgnoreFilter
65 | {
66 | private readonly string _filter;
67 |
68 | public EndsWithFilter(string filter)
69 | {
70 | _filter = filter;
71 | }
72 |
73 | public bool Match(VideoFile video)
74 | {
75 | return Match(video.FilePath);
76 | }
77 |
78 | public bool Match(string filepath)
79 | {
80 | return _filter.EndsWith(filepath, StringComparison.OrdinalIgnoreCase);
81 | }
82 | }
83 |
84 | internal class FuzzyFilter : IVideoIgnoreFilter
85 | {
86 | private readonly string[] filterParts;
87 |
88 | public FuzzyFilter(string filter)
89 | {
90 | filterParts = filter.Split('*');
91 | }
92 | public bool Match(VideoFile video)
93 | {
94 | return Match(video.FilePath);
95 | }
96 |
97 | public bool Match(string filepath)
98 | {
99 | var pos = 0;
100 | for (var i = 0; i < filterParts.Length; i++)
101 | {
102 | var part = filterParts[i];
103 | pos = filepath.IndexOf(part, pos, StringComparison.OrdinalIgnoreCase);
104 | if (pos == -1)
105 | {
106 | return false;
107 | }
108 | }
109 | return true;
110 | }
111 | }
112 |
113 | internal class PassthroughFilter : IVideoIgnoreFilter
114 | {
115 | public bool Match(string filepath) => false;
116 | public bool Match(VideoFile video) => false;
117 | }
118 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/VideoSyncList.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace SubSyncLib.Logic
7 | {
8 | public class VideoSyncList : IVideoSyncList
9 | {
10 | private const string SyncListFile = ".sync-cache";
11 | private readonly HashSet list = new HashSet();
12 | private readonly object mutex = new object();
13 |
14 | public VideoSyncList()
15 | {
16 | if (System.IO.File.Exists(SyncListFile))
17 | {
18 | list = new HashSet(System.IO.File.ReadAllLines(SyncListFile).Where(x => !string.IsNullOrEmpty(x)));
19 | }
20 | }
21 |
22 | public bool Contains(VideoFile video)
23 | {
24 | lock (mutex)
25 | {
26 | return list.Contains(video.Name);
27 | }
28 | }
29 |
30 | public void Add(VideoFile video)
31 | {
32 | lock (mutex)
33 | {
34 | list.Add(video.Name);
35 | }
36 | }
37 |
38 | public void Save()
39 | {
40 | lock (mutex)
41 | {
42 | var sb = new StringBuilder();
43 | foreach (var item in list) sb.AppendLine(item);
44 | System.IO.File.WriteAllText(SyncListFile, sb.ToString());
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/Worker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.CompilerServices;
6 | using System.Threading.Tasks;
7 | using SharpCompress.Archives;
8 | using SharpCompress.Common;
9 | using SharpCompress.Readers;
10 | using SharpCompress.Readers.Rar;
11 | using SubSyncLib.Logic.Exceptions;
12 |
13 | namespace SubSyncLib.Logic
14 | {
15 | public class Worker : IWorker
16 | {
17 | private readonly VideoFile video;
18 | private readonly ILogger logger;
19 | private readonly IWorkerQueue workerQueue;
20 | private readonly ISubtitleProvider subtitleProvider;
21 | private readonly IStatusReporter statusReporter;
22 | private readonly HashSet subtitleExtensions;
23 | private readonly int retryCount;
24 |
25 | private static readonly HashSet FileCompressionExtensions = new HashSet
26 | {
27 | ".zip", ".rar", ".gzip", ".gz", ".7z", ".tar", ".tar.gz"
28 | };
29 |
30 | private TaskCompletionSource taskCompletionSource = null;
31 |
32 | public Worker(
33 | VideoFile video,
34 | ILogger logger,
35 | IWorkerQueue workerQueue,
36 | ISubtitleProvider subtitleProvider,
37 | IStatusReporter statusReporter,
38 | HashSet subtitleExtensions,
39 | int retryCount = 0)
40 | {
41 | this.video = video;
42 | this.logger = logger;
43 | this.workerQueue = workerQueue;
44 | this.subtitleProvider = subtitleProvider;
45 | this.statusReporter = statusReporter;
46 | this.subtitleExtensions = subtitleExtensions;
47 | this.retryCount = retryCount;
48 | }
49 |
50 | public Task SyncAsync()
51 | {
52 | if (taskCompletionSource != null)
53 | {
54 | return taskCompletionSource.Task;
55 | }
56 |
57 | taskCompletionSource = new TaskCompletionSource();
58 |
59 | try
60 | {
61 | return taskCompletionSource.Task;
62 | }
63 | finally
64 | {
65 | Task.Factory.StartNew(async () =>
66 | {
67 | if (retryCount > 0)
68 | {
69 | await Task.Delay(retryCount * 1000);
70 | }
71 |
72 | //var file = new FileInfo(filePath);
73 | logger.WriteLine($"Synchronizing {video.Name}");
74 | try
75 | {
76 | var outputName = await subtitleProvider.GetAsync(video);
77 | var extension = Path.GetExtension(outputName);
78 | if (IsCompressed(extension))
79 | {
80 | outputName = await DecompressAsync(outputName);
81 | }
82 |
83 | var finalName = await RenameAsync(outputName, Path.GetFileNameWithoutExtension(video.Name));
84 | logger.WriteLine(
85 | $"@gray@Subtitle @white@{Path.GetFileName(finalName)} @green@downloaded!");
86 | statusReporter.Report(new WorkerStatus(true, video));
87 | taskCompletionSource.SetResult(true);
88 | }
89 | catch (NestedArchiveNotSupportedException nexc)
90 | {
91 | logger.Error($"Synchronization of {video.Name} failed with: {nexc.Message}");
92 | statusReporter.Report(new WorkerStatus(false, video));
93 | taskCompletionSource.SetException(nexc);
94 | }
95 | catch (Exception exc)
96 | {
97 | logger.Error($"Synchronization of {video.Name} failed with: {exc.Message}");
98 | if (!workerQueue.Enqueue(video)) // (this);
99 | {
100 | statusReporter.Report(new WorkerStatus(false, video));
101 | }
102 | taskCompletionSource.SetException(exc);
103 | }
104 |
105 | }, TaskCreationOptions.LongRunning);
106 |
107 | }
108 | }
109 |
110 | public void Dispose() { }
111 |
112 | private async Task RenameAsync(string fileToRename, string newFilaNameWithoutExtension, bool retried = false)
113 | {
114 | var inFile = new FileInfo(fileToRename);
115 | var directory = inFile.Directory?.FullName ?? "./";
116 | var destFileName = Path.Combine(directory, newFilaNameWithoutExtension + Path.GetExtension(fileToRename));
117 | if (!inFile.Exists)
118 | {
119 | return null;
120 | }
121 | if (System.IO.File.Exists(destFileName))
122 | {
123 | System.IO.File.Delete(destFileName);
124 | }
125 |
126 | try
127 | {
128 | inFile.MoveTo(destFileName);
129 | }
130 | catch (FileNotFoundException exc)
131 | {
132 | // r/quityourbullshit
133 | // file exists, problem is that we accessed it to quickly.
134 | if (retried)
135 | {
136 | return null;
137 | }
138 | await Task.Delay(100);
139 | return await RenameAsync(fileToRename, newFilaNameWithoutExtension, true);
140 | }
141 |
142 | return destFileName;
143 | }
144 |
145 | private Task DecompressAsync(string filename)
146 | {
147 | switch (Path.GetExtension(filename)?.ToLower())
148 | {
149 | case ".rar": return DecompressRarAsync(filename);
150 | case ".zip": return DecompressZipAsync(filename);
151 | case ".7z": return Decompress7ZipAsync(filename);
152 | case ".tar": return DecompressTarAsync(filename);
153 | case ".gz":
154 | case ".gzip":
155 | return DecompressGZipAsync(filename);
156 |
157 | default:
158 | throw new NotImplementedException($"The archive extension: {Path.GetExtension(filename)?.ToLower()} has not yet been implemented.");
159 | }
160 | }
161 |
162 | private async Task DecompressArchive(string filename, Func archiveOpener)
163 | {
164 | var file = new FileInfo(filename);
165 | var targetFile = string.Empty;
166 | try
167 | {
168 | var fileDirectory = file.Directory;
169 | var directory = fileDirectory?.FullName ?? "./";
170 | using (var fileReader = file.OpenRead())
171 | using (var reader = archiveOpener(fileReader))
172 | {
173 | if (reader.IsSolid)
174 | {
175 | var entryReader = reader.ExtractAllEntries();
176 | while (entryReader.MoveToNextEntry())
177 | {
178 | var result = await UnpackSubtitleEntryAsync(entryReader, entryReader.Entry, directory);
179 | if (result.SubtitleFound)
180 | {
181 | targetFile = result.Filename;
182 | return result.Filename;
183 | }
184 | }
185 | }
186 | else
187 | {
188 | foreach (var entry in reader.Entries)
189 | {
190 | var result = await UnpackSubtitleEntryAsync(entry, directory);
191 | if (result.SubtitleFound)
192 | {
193 | targetFile = result.Filename;
194 | return result.Filename;
195 | }
196 | }
197 | }
198 | // TODO: This need to match the subtitles to determine whether its for the right video or not.
199 | // check if any of the entries are archives and unpack it if one exists.
200 | var archive = reader.Entries.FirstOrDefault(x => IsCompressed(Path.GetExtension(x.Key)));
201 | if (archive != null)
202 | {
203 | logger.WriteLine($"@yel@Warning: Nested archive found inside '{filename}', output subtitle may not be correct!");
204 | var result = await UnpackEntryAsync(archive, directory);
205 | targetFile = result.Filename;
206 | return await DecompressAsync(result.Filename);
207 | //throw new NestedArchiveNotSupportedException(filename);
208 | }
209 | }
210 | }
211 | finally
212 | {
213 | if (!string.IsNullOrEmpty(targetFile))
214 | {
215 | file.Delete();
216 | }
217 | }
218 |
219 | throw new FileNotFoundException($"No suitable subtitle found in the downloaded archive, {filename}. Archive kept just in case.");
220 | }
221 |
222 | private async Task UnpackSubtitleEntryAsync(IReader reader, IEntry entry, string directory)
223 | {
224 | if (!TestEntryAsSubtitle(entry, out var unpackSubtitleEntryAsync)) return unpackSubtitleEntryAsync;
225 |
226 | return await UnpackEntryAsync(reader, entry, directory);
227 | }
228 |
229 | private async Task UnpackSubtitleEntryAsync(IArchiveEntry entry, string directory)
230 | {
231 | if (!TestEntryAsSubtitle(entry, out var unpackSubtitleEntryAsync)) return unpackSubtitleEntryAsync;
232 |
233 | return await UnpackEntryAsync(entry, directory);
234 | }
235 |
236 | private bool TestEntryAsSubtitle(IEntry entry, out EntryUnpackResult unpackSubtitleEntryAsync)
237 | {
238 | unpackSubtitleEntryAsync =
239 | new EntryUnpackResult(subtitleFound: false, filename: null, entry: entry.Key);
240 |
241 | var ext = Path.GetExtension(entry.Key);
242 | if (entry.Key.ToLower().EndsWith(".srt.txt"))
243 | {
244 | ext = ".srt";
245 | }
246 |
247 | var subtitleFound = subtitleExtensions.Contains(ext?.ToLower());
248 | if (!subtitleFound)
249 | {
250 | return false;
251 | }
252 |
253 | return true;
254 | }
255 |
256 | private Task UnpackEntryAsync(IReader reader, IEntry entry, string directory)
257 | {
258 | return this.UnpackEntryAsync(reader.OpenEntryStream, entry, directory);
259 | }
260 |
261 | private Task UnpackEntryAsync(IArchiveEntry entry, string directory)
262 | {
263 | return this.UnpackEntryAsync(entry.OpenEntryStream, entry, directory);
264 | }
265 |
266 | private async Task UnpackEntryAsync(Func entryStreamProvider, IEntry entry, string directory)
267 | {
268 | var ext = Path.GetExtension(entry.Key);
269 | if (entry.Key.ToLower().EndsWith(".srt.txt")) ext = ".srt";
270 | var subtitleFound = subtitleExtensions.Contains(ext?.ToLower());
271 | var targetFile = Path.Combine(directory, Path.ChangeExtension(entry.Key.Replace("?", ""), ext));
272 | var dir = new FileInfo(targetFile).Directory;
273 | if (dir != null && !dir.Exists)
274 | {
275 | dir.Create();
276 | }
277 |
278 | var targetFileInfo = new FileInfo(targetFile);
279 | using (var entryStream = entryStreamProvider())
280 | using (var sw = targetFileInfo.Create())//new FileStream(targetFile, FileMode.Create))
281 | {
282 | var read = 0;
283 | var buffer = new byte[4096];
284 | while ((read = await entryStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
285 | {
286 | await sw.WriteAsync(buffer, 0, read);
287 | }
288 |
289 | return new EntryUnpackResult(subtitleFound: subtitleFound, filename: targetFile, entry: entry.Key);
290 | }
291 | }
292 |
293 | private Task DecompressRarAsync(string filename) => DecompressArchive(filename, x => SharpCompress.Archives.Rar.RarArchive.Open(x));
294 | private Task DecompressZipAsync(string filename) => DecompressArchive(filename, x => SharpCompress.Archives.Zip.ZipArchive.Open(x));
295 | private Task DecompressGZipAsync(string filename) => DecompressArchive(filename, x => SharpCompress.Archives.GZip.GZipArchive.Open(x));
296 | private Task Decompress7ZipAsync(string filename) => DecompressArchive(filename, x => SharpCompress.Archives.SevenZip.SevenZipArchive.Open(x));
297 | private Task DecompressTarAsync(string filename) => DecompressArchive(filename, x => SharpCompress.Archives.Tar.TarArchive.Open(x));
298 |
299 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
300 | private static bool IsCompressed(string extension) => FileCompressionExtensions.Contains(extension.ToLower());
301 |
302 | private struct EntryUnpackResult
303 | {
304 | public readonly bool SubtitleFound;
305 | public readonly string Filename;
306 | public readonly string Entry;
307 |
308 | public EntryUnpackResult(bool subtitleFound, string filename, string entry)
309 | {
310 | SubtitleFound = subtitleFound;
311 | Filename = filename;
312 | Entry = entry;
313 | }
314 | }
315 | }
316 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/WorkerProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace SubSyncLib.Logic
4 | {
5 | public class WorkerProvider : IWorkerProvider
6 | {
7 | private readonly ILogger logger;
8 | private readonly ISubtitleProvider subtitleProvider;
9 | private readonly IStatusReporter statusReporter;
10 | private readonly HashSet subtitleExtensions;
11 |
12 | public WorkerProvider(
13 | ILogger logger,
14 | HashSet subtitleExtensions,
15 | ISubtitleProvider subtitleProvider,
16 | IStatusReporter statusReporter)
17 | {
18 | this.logger = logger;
19 | this.subtitleProvider = subtitleProvider;
20 | this.statusReporter = statusReporter;
21 | this.subtitleExtensions = subtitleExtensions;
22 | }
23 |
24 | public IWorker GetWorker(IWorkerQueue queue, VideoFile video, int tryCount = 0)
25 | {
26 | return new Worker(
27 | video,
28 | logger,
29 | queue,
30 | subtitleProvider,
31 | statusReporter,
32 | subtitleExtensions,
33 | tryCount);
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/WorkerQueue.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace SubSyncLib.Logic
8 | {
9 | public class WorkerQueue : IWorkerQueue
10 | {
11 | private const int ConcurrentWorkers = 5;
12 | private readonly SubSyncSettings settings;
13 | private readonly IWorkerProvider workerProvider;
14 | private readonly IStatusReporter statusReporter;
15 | private readonly ConcurrentQueue queue = new ConcurrentQueue();
16 | private readonly ConcurrentDictionary queueTries = new ConcurrentDictionary();
17 | private readonly Thread workerThread;
18 | private bool enabled;
19 | private bool disposed;
20 |
21 | private int activeJobCount = 0;
22 |
23 |
24 | // the max times the same item can be enqueued.
25 | private const int RetryLimit = 3;
26 |
27 | public WorkerQueue(SubSyncSettings settings, IWorkerProvider workerProvider, IStatusReporter statusReporter)
28 | {
29 | this.settings = settings;
30 | this.workerProvider = workerProvider;
31 | this.statusReporter = statusReporter;
32 | workerThread = new Thread(ProcessQueue);
33 | }
34 |
35 | public int Count => queue.Count;
36 |
37 | public int Active => activeJobCount;
38 |
39 | public void Dispose()
40 | {
41 | if (disposed) return;
42 | Stop();
43 | disposed = true;
44 | }
45 |
46 | public bool Enqueue(VideoFile video)
47 | {
48 | queueTries.TryGetValue(video.Name, out var tries);
49 | if (tries < RetryLimit)
50 | {
51 | queueTries[video.Name] = tries + 1;
52 | queue.Enqueue(workerProvider.GetWorker(this, video, tries));
53 | return true;
54 | }
55 |
56 | return false;
57 | }
58 |
59 | public void Start()
60 | {
61 | if (enabled) return;
62 | enabled = true;
63 | workerThread.Start();
64 | }
65 |
66 | public void Stop()
67 | {
68 | if (!enabled) return;
69 | enabled = false;
70 | workerThread.Join();
71 | }
72 |
73 | public void Reset()
74 | {
75 | while (queue.TryDequeue(out _)) ;
76 | queueTries.Clear();
77 | }
78 |
79 | private async void ProcessQueue()
80 | {
81 | var activeJobs = new List();
82 | do
83 | {
84 | while (activeJobs.Count < ConcurrentWorkers && queue.TryDequeue(out var worker))
85 | {
86 | Interlocked.Increment(ref activeJobCount);
87 |
88 | if (settings.MinimummDelayBetweenRequests > 0)
89 | {
90 | await Task.Delay(settings.MinimummDelayBetweenRequests);
91 | }
92 |
93 | activeJobs.Add(worker.SyncAsync());
94 | }
95 |
96 | if (activeJobs.Count > 0)
97 | {
98 | await Task.WhenAny(activeJobs);
99 | activeJobs = activeJobs.Where(x => !x.IsCompleted).ToList();
100 |
101 | Volatile.Write(ref activeJobCount, activeJobs.Count);
102 | }
103 | else
104 | {
105 | statusReporter.FinishReport();// should only report if it has been set dirty. This happens only if someone has reported data.
106 | Thread.Sleep(100);
107 | }
108 |
109 | } while (enabled);
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/WorkerStatus.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic
2 | {
3 | public struct WorkerStatus
4 | {
5 | public readonly bool Succeeded;
6 | public readonly VideoFile Target;
7 |
8 | public WorkerStatus(bool succeeded, VideoFile target)
9 | {
10 | Succeeded = succeeded;
11 | Target = target;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/IXmlRpcObjectValue.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic.XmlRpc
2 | {
3 | public interface IXmlRpcObjectValue
4 | {
5 | object GetValue();
6 | }
7 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcArray.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic.XmlRpc
2 | {
3 | public class XmlRpcArray : XmlRpcObjectBase
4 | {
5 | public XmlRpcObjectBase[] Items { get; }
6 |
7 | public XmlRpcArray(XmlRpcObjectBase[] items)
8 | {
9 | Items = items;
10 | }
11 |
12 | public override XmlRpcObjectBase FindRecursive(string name)
13 | {
14 | foreach (var item in Items)
15 | {
16 | var found = item.FindRecursive(name);
17 | if (found != null)
18 | {
19 | return found;
20 | }
21 | }
22 | return null;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcDouble.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic.XmlRpc
2 | {
3 | public class XmlRpcDouble : XmlRpcValueObject
4 | {
5 | public double Value { get; }
6 |
7 | public XmlRpcDouble(double value)
8 | {
9 | Value = value;
10 | }
11 |
12 | public override string ToString()
13 | {
14 | return Value.ToString();
15 | }
16 |
17 | public static implicit operator double(XmlRpcDouble val)
18 | {
19 | return val.Value;
20 | }
21 |
22 | public override object GetValue()
23 | {
24 | return Value;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcInt.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic.XmlRpc
2 | {
3 | public class XmlRpcInt : XmlRpcValueObject
4 | {
5 | public int Value { get; }
6 |
7 | public XmlRpcInt(int value)
8 | {
9 | Value = value;
10 | }
11 |
12 | public override string ToString()
13 | {
14 | return Value.ToString();
15 | }
16 |
17 | public static implicit operator int(XmlRpcInt val)
18 | {
19 | return val.Value;
20 | }
21 |
22 | public override object GetValue()
23 | {
24 | return Value;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcMember.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SubSyncLib.Logic.XmlRpc
4 | {
5 | public class XmlRpcMember : XmlRpcObjectBase
6 | {
7 | public XmlRpcString Name { get; }
8 | public XmlRpcObjectBase Value { get; }
9 |
10 | public XmlRpcMember(XmlRpcString name, XmlRpcObjectBase value)
11 | {
12 | Name = name;
13 | Value = value;
14 | }
15 |
16 | public override string ToString()
17 | {
18 | return "'" + Name + "' => '" + Value + "'";
19 | }
20 |
21 | public override XmlRpcObjectBase FindRecursive(string name)
22 | {
23 | if (name.Equals(Name.ToString(), StringComparison.OrdinalIgnoreCase))
24 | {
25 | return this;
26 | }
27 |
28 | return null;
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcObject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Reflection;
5 |
6 | namespace SubSyncLib.Logic.XmlRpc
7 | {
8 | public class XmlRpcObject : XmlRpcObjectBase
9 | {
10 | public readonly List children = new List();
11 |
12 | public void Add(XmlRpcObjectBase child)
13 | {
14 | children.Add(child);
15 | }
16 |
17 | public T GetValue(string name)
18 | {
19 | if (children.Count > 0 && !string.IsNullOrEmpty(name))
20 | {
21 | foreach (var child in children)
22 | {
23 | var node = child.FindRecursive(name);
24 | if (node is XmlRpcMember member)
25 | {
26 | node = member.Value;
27 | }
28 | if (node is IXmlRpcObjectValue valueNode)
29 | {
30 | return (T)valueNode.GetValue();
31 | }
32 | }
33 | }
34 | return default(T);
35 | }
36 |
37 | public override XmlRpcObjectBase FindRecursive(string name)
38 | {
39 | foreach (var child in children)
40 | {
41 | var found = child.FindRecursive(name);
42 | if (found != null)
43 | {
44 | return found;
45 | }
46 | }
47 |
48 | return null;
49 | }
50 |
51 | public T Deserialize()
52 | {
53 | var dataRoot = "data";
54 | var type = typeof(T);
55 | if (type.IsArray)
56 | {
57 | var elementType = type.GetElementType();
58 | return DeserializeArray(dataRoot, elementType, elementType.GetProperties());
59 | }
60 |
61 | if (type.IsGenericType)
62 | {
63 | var genericTypeDefinition = type.GetGenericTypeDefinition();
64 | return DeserializeGeneric(dataRoot, genericTypeDefinition, genericTypeDefinition.GetProperties());
65 | }
66 |
67 | return Deserialize(dataRoot, type.GetProperties(BindingFlags.Public));
68 | }
69 |
70 |
71 | private T Deserialize(string dataRoot, PropertyInfo[] properties)
72 | {
73 | throw new NotImplementedException();
74 | return default(T);
75 | }
76 |
77 | private T DeserializeGeneric(string dataRoot, Type genericType, PropertyInfo[] properties)
78 | {
79 | throw new NotImplementedException();
80 | return default(T);
81 | }
82 |
83 | private T DeserializeArray(string dataRoot, Type elementType, PropertyInfo[] properties)
84 | {
85 | var list = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) as IList;
86 | //var listAdd = list.GetType().GetMethod("Add", BindingFlags.Public);
87 |
88 | if (FindRecursive(dataRoot) is XmlRpcMember member)
89 | {
90 | if (member.Value is XmlRpcArray array)
91 | {
92 | foreach (var item in array.Items)
93 | {
94 | var value = Deserialize(elementType, item, properties);
95 | list.Add(Convert.ChangeType(value, elementType));
96 | }
97 | }
98 | }
99 |
100 | var result = Array.CreateInstance(elementType, list.Count);
101 | var index = 0;
102 | foreach (var item in list)
103 | {
104 | result.SetValue(item, index++);
105 | }
106 |
107 |
108 | return (T)(object)result;
109 | }
110 |
111 | private object Deserialize(Type targetType, XmlRpcObjectBase item, PropertyInfo[] properties)
112 | {
113 | if (item is XmlRpcStruct strct)
114 | {
115 | return DeserializeStruct(targetType, strct, properties);
116 | }
117 |
118 | // todo: implement any other types that may be supported like serialize as strings, integers, doubles, etc
119 | throw new NotImplementedException();
120 |
121 | return default(object);
122 | }
123 |
124 | private object DeserializeStruct(Type structType, XmlRpcStruct structData, PropertyInfo[] properties)
125 | {
126 | var instance = Activator.CreateInstance(structType);
127 | foreach (var prop in properties)
128 | {
129 | if (structData.FindRecursive(prop.Name) is XmlRpcMember member)
130 | {
131 | if (member.Value is IXmlRpcObjectValue val)
132 | {
133 | var v = val.GetValue();
134 | prop.SetValue(instance, Convert.ChangeType(v, prop.PropertyType));
135 | }
136 | }
137 | }
138 | return instance;
139 | }
140 | }
141 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcObjectBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Xml.Linq;
4 |
5 | namespace SubSyncLib.Logic.XmlRpc
6 | {
7 | public abstract class XmlRpcObjectBase
8 | {
9 | protected XmlRpcObjectBase()
10 | {
11 | }
12 |
13 | public static XmlRpcObject Parse(string data)
14 | {
15 | var doc = XDocument.Parse(data);
16 | if (doc.Root == null)
17 | {
18 | throw new ArgumentException("Argument is not a valid xml document.", nameof(data));
19 | }
20 |
21 | if (doc.Root.Name == "methodResponse")
22 | {
23 | return ParseMethodResponse(doc.Root);
24 | }
25 |
26 | return new XmlRpcObject();
27 | }
28 |
29 | private static XmlRpcObject ParseMethodResponse(XElement doc)
30 | {
31 | var values = doc.Element("params");
32 | if (values == null)
33 | {
34 | throw new Exception("Xmlrpc response is invalid, missing params element.");
35 | }
36 |
37 | var resultObject = new XmlRpcObject();
38 | var parameters = values.Elements("param");
39 | foreach (var param in parameters)
40 | {
41 | var result = ParseParam(param);
42 | resultObject.Add(result);
43 | }
44 |
45 | return resultObject;
46 | }
47 |
48 |
49 | public abstract XmlRpcObjectBase FindRecursive(string name);
50 |
51 | private static XmlRpcObjectBase ParseParam(XElement param)
52 | {
53 | return ParseValue(param.Element("value"));
54 | }
55 |
56 | private static XmlRpcObjectBase ParseValue(XElement value)
57 | {
58 | foreach (var elm in value.Elements())
59 | {
60 | if (elm.Name == "struct")
61 | {
62 | return ParseStruct(elm);
63 | }
64 | if (elm.Name == "array")
65 | {
66 | return ParseArray(elm);
67 | }
68 | if (elm.Name == "string")
69 | {
70 | return ParseString(elm);
71 | }
72 | if (elm.Name == "double")
73 | {
74 | return ParseDouble(elm);
75 | }
76 | if (elm.Name == "int")
77 | {
78 | return ParseInt(elm);
79 | }
80 | }
81 |
82 | return ParseString(value);
83 | }
84 |
85 | private static XmlRpcInt ParseInt(XElement elm)
86 | {
87 | int.TryParse(elm.Value, out var value);
88 | return new XmlRpcInt(value);
89 | }
90 |
91 | private static XmlRpcDouble ParseDouble(XElement elm)
92 | {
93 | double.TryParse(elm.Value, out var value);
94 | return new XmlRpcDouble(value);
95 | }
96 |
97 | private static XmlRpcObjectBase ParseString(XElement elm)
98 | {
99 | return new XmlRpcString(elm.Value);
100 | }
101 |
102 | private static XmlRpcArray ParseArray(XElement elm)
103 | {
104 | var items = new List();
105 | var dataElement = elm.Element("data");
106 | var elements = dataElement.Elements("value");
107 | foreach (var elmData in elements)
108 | {
109 | var item = ParseValue(elmData);
110 | items.Add(item);
111 | }
112 |
113 | return new XmlRpcArray(items.ToArray());
114 | }
115 |
116 | private static XmlRpcObjectBase ParseStruct(XElement elm)
117 | {
118 | var foundMembers = new List();
119 | var members = elm.Elements("member");
120 | foreach (var member in members)
121 | {
122 | var name = ParseValue(member.Element("name")) as XmlRpcString;
123 | var value = ParseValue(member.Element("value"));
124 | foundMembers.Add(new XmlRpcMember(name, value));
125 | }
126 | return new XmlRpcStruct(foundMembers);
127 | }
128 |
129 | public static T Deserialize(string data)
130 | {
131 | // aint gonna write a xml parser today
132 | // 1. parse as xml or xdoc
133 | // 2. get properties of type T
134 | // 3. create instance of T
135 | // 4. assign all properties with values matching same name parsed from data.
136 | //
137 |
138 | return Parse(data).Deserialize();
139 | }
140 | }
141 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcString.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic.XmlRpc
2 | {
3 | public class XmlRpcString : XmlRpcValueObject
4 | {
5 | public string Value { get; }
6 |
7 | public XmlRpcString(string value)
8 | {
9 | Value = value;
10 | }
11 |
12 | public override string ToString()
13 | {
14 | return Value;
15 | }
16 |
17 | public static implicit operator string(XmlRpcString val)
18 | {
19 | return val.Value;
20 | }
21 |
22 | public override object GetValue()
23 | {
24 | return Value;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcStruct.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace SubSyncLib.Logic.XmlRpc
4 | {
5 | public class XmlRpcStruct : XmlRpcObjectBase
6 | {
7 | public List Members { get; }
8 |
9 | public XmlRpcStruct(List members)
10 | {
11 | Members = members;
12 | }
13 |
14 | public override XmlRpcObjectBase FindRecursive(string name)
15 | {
16 | foreach (var item in Members)
17 | {
18 | var found = item.FindRecursive(name);
19 | if (found != null)
20 | {
21 | return found;
22 | }
23 | }
24 | return null;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Logic/XmlRpc/XmlRpcValueObject.cs:
--------------------------------------------------------------------------------
1 | namespace SubSyncLib.Logic.XmlRpc
2 | {
3 | public abstract class XmlRpcValueObject : XmlRpcObjectBase, IXmlRpcObjectValue
4 | {
5 | public override XmlRpcObjectBase FindRecursive(string name)
6 | {
7 | return null;
8 | }
9 |
10 | public abstract object GetValue();
11 | }
12 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Runtime.CompilerServices;
6 | using SubSyncLib.Logic;
7 | using SubSyncLib.Providers;
8 |
9 | [assembly: InternalsVisibleTo("SubSync.Tests")]
10 | namespace SubSyncLib
11 | {
12 | public class SubSyncSettings
13 | {
14 | [StartupArgument(0, "./")]
15 | public string Input { get; set; }
16 |
17 | [StartupArgument("lang", "english")]
18 | public HashSet Languages { get; set; }
19 |
20 | [StartupArgument("vid", "*.avi;*.mp4;*.mkv;*.mpeg;*.flv;*.webm")]
21 | public HashSet VideoExt { get; set; }
22 |
23 | [StartupArgument("sub", "*.srt;*.txt;*.sub;*.idx;*.ssa;*.ass")]
24 | public HashSet SubtitleExt { get; set; }
25 |
26 | [StartupArgument("exit")]
27 | public bool ExitAfterSync { get; set; }
28 |
29 | // using this will force requests to be sequential rather than concurrent
30 | [StartupArgument("delay", "0")]
31 | public int MinimummDelayBetweenRequests { get; set; }
32 |
33 | [StartupArgument("resync")]
34 | public bool Resync { get; set; }
35 |
36 | [StartupArgument("resyncall")]
37 | public bool ResyncAll { get; set; }
38 | }
39 |
40 | public class Program
41 | {
42 | public static void Main(string[] args)
43 | {
44 | var settings = Arguments.Parse(args);
45 |
46 | //// ugly workaround for ending backslashes with single/double-quotes. Seem to be a bug in the dotnet!
47 | //if (!string.IsNullOrEmpty(settings.Input) && (settings.Input.EndsWith("\"") || settings.Input.EndsWith("'")))
48 | //{
49 | // settings.Input = settings.Input.Substring(0, settings.Input.Length - 1);
50 | //}
51 |
52 | var subtitleExtensions = settings.SubtitleExt;
53 | var languages = settings.Languages;
54 | var input = settings.Input;
55 |
56 | var version = GetVersion();
57 | var logger = new ConsoleLogger();
58 | var videoIgnoreFilter = new VideoIgnoreFilter(ReadVideoIgnoreList());
59 | var videoSyncList = new VideoSyncList();
60 | using (var fallbackSubtitleProvider = new FallbackSubtitleProvider(
61 | videoSyncList,
62 | new OpenSubtitles(languages, new FileBasedCredentialsProvider("opensubtitle.auth", logger), logger)))
63 | //new Subscene(languages)))
64 | {
65 | var resultReporter = new QueueProcessReporter();
66 | var subSyncWorkerProvider = new WorkerProvider(logger, subtitleExtensions, fallbackSubtitleProvider, resultReporter);
67 | var subSyncWorkerQueue = new WorkerQueue(settings, subSyncWorkerProvider, resultReporter);
68 |
69 | using (var mediaWatcher = new SubtitleSynchronizer(
70 | logger, videoSyncList, subSyncWorkerQueue, resultReporter, videoIgnoreFilter, settings))
71 | {
72 | logger.WriteLine("╔════════════════════════════════════════════╗");
73 | logger.WriteLine("║ @whi@SubSync v" + version.PadRight(30 - version.Length) + "@gray@ ║");
74 | logger.WriteLine("║ -------------------------------------- ║");
75 | logger.WriteLine("║ Copyright (c) 2018 zerratar\\@gmail.com ║");
76 | logger.WriteLine("╚════════════════════════════════════════════╝");
77 | logger.WriteLine("");
78 | logger.WriteLine(" Following folder and its subfolders being watched");
79 | logger.WriteLine($" @whi@{input} @gray@");
80 | logger.WriteLine("");
81 | logger.WriteLine(" You may press @green@'q' @gray@at any time to quit.");
82 | logger.WriteLine("");
83 |
84 | if (settings.MinimummDelayBetweenRequests > 0)
85 | {
86 | logger.WriteLine($" @yel@Request delay: @red@{settings.MinimummDelayBetweenRequests}ms @yel@activated.");
87 | logger.WriteLine($" @yel@All requests will be run in sequential order and may take a lot longer to sync.");
88 | logger.WriteLine("");
89 | }
90 |
91 | logger.WriteLine(" ───────────────────────────────────────────────────── ");
92 | logger.WriteLine("");
93 |
94 | mediaWatcher.Start();
95 | ConsoleKeyInfo ck;
96 | while ((ck = Console.ReadKey(true)).Key != ConsoleKey.Q)
97 | {
98 | if (ck.Key == ConsoleKey.A)
99 | {
100 | mediaWatcher.SyncAll();
101 | }
102 | System.Threading.Thread.Sleep(10);
103 | }
104 | }
105 | }
106 | }
107 |
108 |
109 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
110 | private static List ReadVideoIgnoreList()
111 | {
112 | const string vidignore = ".vidignore";
113 | return System.IO.File.Exists(vidignore)
114 | ? ReadVidIgnoreList(System.IO.File.ReadAllLines(vidignore))
115 | : new List();
116 | }
117 |
118 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
119 | private static List ReadVidIgnoreList(string[] lines)
120 | {
121 | return lines.Where(x => !string.IsNullOrEmpty(x.Trim()) && !x.Trim().StartsWith("#")).ToList();
122 | }
123 |
124 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
125 | private static string GetVersion()
126 | {
127 | var assembly = System.Reflection.Assembly.GetExecutingAssembly();
128 | var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
129 | return fvi.FileVersion;
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/SubSyncLib/Providers/OpenSubtitles.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.CompilerServices;
6 | using System.Text;
7 | using System.Text.RegularExpressions;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using SubSyncLib.Logic;
11 | using SubSyncLib.Logic.Exceptions;
12 | using SubSyncLib.Logic.XmlRpc;
13 |
14 | namespace SubSyncLib.Providers
15 | {
16 | ///
17 | // Implementation of the https://www.opensubtitles.org XML-RPC Api
18 | ///
19 | public class OpenSubtitles : SubtitleProviderBase, IDisposable
20 | {
21 | private const string VipApiUrl = "https://vip-api.opensubtitles.org/xml-rpc";
22 | private const string ApiUrl = "http://api.opensubtitles.org/xml-rpc";
23 | private const int MaxDownloadsPerDay = 200;
24 | private const int VipMaxDownloadsPerDay = 1000;
25 | private const int MaxRequestsEvery10Seconds = 40;
26 | private const int VipMaxRequestsEvery10Seconds = 40;
27 | private readonly int keepAliveInterval = 60 * 14; // every 14 minutes, 15 according to api. but just to be safe.
28 | private readonly AutoResetEvent loginMutex = new AutoResetEvent(true);
29 | private readonly IAuthCredentialProvider credentialProvider;
30 | private readonly ILogger logger;
31 | private readonly HashSet supportedLanguages;
32 | private readonly Thread keepAliveThread;
33 |
34 | private DateTime startTime;
35 | private DateTime requestBlockTimeLimit;
36 | private int totalRequests;
37 | private int totalRequestsToday;
38 | private int totalRequestsInTimeBlock;
39 | private int downloadQuota = 200; // should be updated from the response header: "Download-Quota" if available. Otherwise manually track.
40 |
41 | private string authenticationToken;
42 | private bool isAuthenticated;
43 | private bool isVip;
44 |
45 | private bool disposed;
46 |
47 | public OpenSubtitles(HashSet languages, IAuthCredentialProvider credentialProvider, ILogger logger) : base(languages)
48 | {
49 | this.credentialProvider = credentialProvider;
50 | this.logger = logger;
51 | supportedLanguages = GetSupportedLanguages(languages);
52 |
53 | RequestRetryLimit = 3; // max 3 retries, and with some seconds delay is necessary for opensubtitles
54 | startTime = DateTime.Now.Date;
55 |
56 | // Max 40 requests per 10 seconds per IP
57 | // Max 200 subtitle downloads per 24 hour per IP/User
58 | // User has to register as VIP to download 1000 per 24 hours.
59 | // 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
60 | keepAliveThread = new Thread(KeepAliveProcess);
61 | keepAliveThread.Start();
62 | }
63 |
64 | public void Dispose()
65 | {
66 | if (disposed)
67 | {
68 | return;
69 | }
70 |
71 | disposed = true;
72 | if (isAuthenticated)
73 | {
74 | LogoutAsync();
75 | }
76 |
77 | keepAliveThread.Join();
78 | }
79 |
80 | public override async Task GetAsync(VideoFile video)//string name, string outputDirectory)
81 | {
82 | AssertWithinRequestLimits();
83 | var name = video.Name;
84 | var outputDirectory = video.Directory?.FullName ?? "./";
85 | logger.Debug($"get-async: '{name}'");
86 |
87 | await LoginIfRequiredAsync();
88 |
89 | var searchResults = await SearchSubtitleAsync(video.FilePath);
90 | if (searchResults.Length == 0)
91 | {
92 | throw new SubtitleNotFoundException();
93 | }
94 |
95 | var bestMatchingResult = FindBestSearchResultMatch(name, searchResults);
96 | if (bestMatchingResult == null)
97 | {
98 | throw new SubtitleNotFoundException();
99 | }
100 |
101 | return await DownloadSubtitleAsync(bestMatchingResult, outputDirectory);
102 | }
103 |
104 | // see http://trac.opensubtitles.org/projects/opensubtitles/wiki/XmlRpcSearchSubtitles
105 | private async Task SearchSubtitleAsync(string name)
106 | {
107 | logger.Debug($"@gray@Searching for '{name}'...");
108 |
109 | var languageList = string.Join(",", supportedLanguages.Select(x => x.LanguageId).ToArray());
110 |
111 | // TODO: its preferred to do a search with hash rather than filename
112 | // for later, we could try use search with hash first and then do a query search
113 | // if no results were given in the first search.
114 |
115 | // var movieHash = CalculateVideoHash(name);
116 | // var movieByteSize = GetVideoByteSize(name);
117 |
118 | var query = Path.GetFileName(name);
119 |
120 | var season = "";
121 | var episode = "";
122 | var seasonNumber = 0;
123 | var episodeNumber = 0;
124 | var isTvShowEpisode = false;
125 |
126 | var regex = new Regex(@"([s](?\d+)[e](?\d+))|((?\d+)[s](?\d+))
127 | |((?\d+)[e](?\d+))|([s](?\d+))|(ep(?\d+))
128 | |(season.(?\d+))|(episode.(?\d+))|(e(?\d+))",
129 | RegexOptions.IgnoreCase);
130 |
131 | foreach (Match m in regex.Matches(query))
132 | {
133 | var episodeGroup = m.Groups["episode"];
134 | if (episodeGroup.Success && string.IsNullOrEmpty(episode))
135 | {
136 | var item = episodeGroup.Captures[0];
137 | if (item != null && !string.IsNullOrEmpty(item.Value))
138 | {
139 | episode = item.Value;
140 | }
141 | }
142 |
143 | var seasonGroup = m.Groups["season"];
144 | if (seasonGroup.Success && string.IsNullOrEmpty(season))
145 | {
146 | var item = seasonGroup.Captures[0];
147 | if (item != null && !string.IsNullOrEmpty(item.Value))
148 | {
149 | season = item.Value;
150 | }
151 | }
152 |
153 | if (!string.IsNullOrEmpty(episode) && !string.IsNullOrEmpty(season))
154 | {
155 | isTvShowEpisode = true;
156 | int.TryParse(episode, out episodeNumber);
157 | int.TryParse(season, out seasonNumber);
158 | break;
159 | }
160 | }
161 |
162 | XmlRpcObject requestResult = null;
163 | if (isTvShowEpisode)
164 | {
165 | logger.Debug($"@gray@Searching with query '{query}'...");
166 | //query = regex.Replace(query, "");
167 | requestResult = await ApiRequest("SearchSubtitles",
168 | Arg("query", query),
169 | Arg("sublanguageid", languageList),
170 | Arg("seriesepisode", episodeNumber),
171 | Arg("Seriesseason", seasonNumber));
172 | }
173 | else
174 | {
175 | logger.Debug($"@gray@Searching with query '{query}'...");
176 | requestResult = await ApiRequest("SearchSubtitles",
177 | Arg("query", query),
178 | Arg("sublanguageid", languageList));
179 | }
180 |
181 | var subtitles = requestResult.Deserialize();
182 | if (subtitles.Length == 0)
183 | {
184 | var movieHash = Utilities.ComputeMovieHash(name);
185 | var movieByteSize = new FileInfo(name).Length;
186 |
187 | requestResult = await ApiRequest("SearchSubtitles",
188 | Arg("moviehash", Utilities.ToHexadecimal(movieHash)),
189 | Arg("moviebytesize", movieByteSize),
190 | Arg("sublanguageid", languageList));
191 |
192 | return requestResult.Deserialize();
193 | }
194 | return subtitles;
195 | }
196 |
197 | private async Task DownloadSubtitleAsync(Subtitle target, string outputDirectory)
198 | {
199 | var quota = Interlocked.Decrement(ref downloadQuota); // is really only counted if the request was successeful. but to be on the safe side.
200 | if (quota <= 0)
201 | {
202 | throw new DownloadQuotaReachedException();
203 | }
204 |
205 | logger.Debug($"@gray@Downloading '@green@{target.MovieReleaseName}@gray@'...");
206 |
207 | var result = await ApiRequest("DownloadSubtitles", Arg(target.IdSubtitleFile));
208 | var subtitles = result.Deserialize();
209 | var subtitle = subtitles.First();
210 | var subtitleData = Utilities.DecompressGzipBase64(subtitle.Data);
211 | var outputFileName = Path.Combine(outputDirectory, target.SubFileName);
212 | File.WriteAllText(outputFileName, subtitleData, Encoding.UTF8);
213 | return outputFileName;
214 | }
215 |
216 | private Subtitle FindBestSearchResultMatch(string name, Subtitle[] searchResults)
217 | {
218 | logger.Debug($"@gray@Finding best match for '{name}'...");
219 | return FilenameDiff.FindBestMatch(name, searchResults, x => x.MovieReleaseName);
220 | }
221 |
222 | private async Task LoginIfRequiredAsync()
223 | {
224 | loginMutex.WaitOne();
225 |
226 | try
227 | {
228 | if (!isAuthenticated)
229 | {
230 | var credentials = credentialProvider.Get();
231 | var authResult = await LoginAsync(credentials);
232 | authenticationToken = authResult.GetValue("token");
233 | if (!string.IsNullOrEmpty(authenticationToken))
234 | {
235 | logger.Debug("OpenSubtitles login @green@successefull");
236 | isAuthenticated = true;
237 | return;
238 | }
239 |
240 | throw new UnauthorizedAccessException("Login to opensubtitle.org failed!");
241 | }
242 | }
243 | finally
244 | {
245 | loginMutex.Set();
246 | }
247 | }
248 |
249 | private Task LogoutAsync()
250 | {
251 | return ApiRequest("LogOut");
252 | }
253 |
254 | private async Task NoOperationAsync()
255 | {
256 | var result = await ApiRequest("NoOperation");
257 | if (!result.GetValue("status").StartsWith("200"))
258 | {
259 | isAuthenticated = false;
260 | isVip = false;
261 | return false;
262 | }
263 |
264 | return true;
265 | }
266 |
267 | private Task LoginAsync(AuthCredentials credentials)
268 | {
269 | authenticationToken = null;
270 | isAuthenticated = false;
271 | isVip = false;
272 | return ApiRequest("LogIn", Arg(credentials.Username), Arg(credentials.Password), Arg("en"), Arg(UserAgent));
273 | }
274 |
275 | private async Task ApiRequest(string method, params KeyValuePair[] arguments)
276 | {
277 | Interlocked.Increment(ref totalRequests);
278 | Interlocked.Increment(ref totalRequestsToday);
279 | Interlocked.Increment(ref totalRequestsInTimeBlock);
280 |
281 | if (requestBlockTimeLimit == DateTime.MinValue)
282 | {
283 | requestBlockTimeLimit = DateTime.Now;
284 | }
285 |
286 | var url = isVip ? VipApiUrl : ApiUrl;
287 | var requestData = BuildRequestData(method, arguments);
288 | var request = CreatePostAsync(url, requestData);
289 | try
290 | {
291 | using (var response = await request.GetResponseAsync())
292 | {
293 | if (response.Headers.HasKeys())
294 | {
295 | var headerKeys = response.Headers.AllKeys;
296 | if (headerKeys.Contains("Content-Location"))
297 | {
298 | isVip = isVip || response.Headers.Get("Content-Location")?.ToLower() ==
299 | "https://vip-api.opensubtitles.org.local/xml-rpc";
300 | }
301 |
302 | if (headerKeys.Contains("Download-Quota"))
303 | {
304 | if (int.TryParse(response.Headers.Get("Download-Quota"), out var quota))
305 | {
306 | Volatile.Write(ref downloadQuota, quota);
307 | }
308 |
309 | }
310 | }
311 |
312 | return XmlRpcObjectBase.Parse(await GetResponseStringAsync(response));
313 | }
314 | }
315 | catch (Exception exc)
316 | {
317 | // for now...
318 | return null;
319 | }
320 | }
321 |
322 | private void KeepAliveProcess()
323 | {
324 | var lastKeepAliveMessage = DateTime.Now;
325 | while (!disposed)
326 | {
327 | if (isAuthenticated)
328 | {
329 | if (DateTime.Now - lastKeepAliveMessage > TimeSpan.FromSeconds(keepAliveInterval))
330 | {
331 | lastKeepAliveMessage = DateTime.Now;
332 | // 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.
333 | NoOperationAsync();
334 | }
335 | }
336 | Thread.Sleep(250);
337 | }
338 | }
339 |
340 | private HashSet GetSupportedLanguages(HashSet languages)
341 | {
342 | var result = new HashSet();
343 | foreach (var language in languages)
344 | {
345 | result.Add(SubtitleLanguage.Find(language));
346 | }
347 | return result;
348 | }
349 |
350 | private void AssertWithinRequestLimits()
351 | {
352 | var elapsedTime = DateTime.Now.Date - startTime;
353 | if (elapsedTime >= TimeSpan.FromDays(1))
354 | {
355 | // its been one day. Reset the counters
356 | Volatile.Write(ref totalRequestsToday, 0);
357 | Volatile.Write(ref downloadQuota, isVip ? VipMaxDownloadsPerDay : MaxDownloadsPerDay);
358 | startTime = DateTime.Now.Date;
359 | }
360 | else
361 | {
362 | var quota = Volatile.Read(ref downloadQuota);
363 | if (quota <= 0)
364 | {
365 | throw new DownloadQuotaReachedException();
366 | }
367 |
368 | var timeBlock = DateTime.Now - requestBlockTimeLimit;
369 | if (timeBlock < TimeSpan.FromSeconds(10))
370 | {
371 | var requests = Volatile.Read(ref totalRequestsInTimeBlock);
372 | if (requests >= (isVip ? VipMaxRequestsEvery10Seconds : MaxRequestsEvery10Seconds))
373 | {
374 | throw new RequestQuotaReachedException();
375 | }
376 | }
377 | else
378 | {
379 | requestBlockTimeLimit = DateTime.Now;
380 | Volatile.Write(ref totalRequestsInTimeBlock, 0);
381 | }
382 | }
383 | }
384 |
385 | private string BuildRequestData(string method, params KeyValuePair[] arguments)
386 | {
387 | // quick and dirty xml-rpc methodcall serialization
388 | // may do this correctly in the future. but meh
389 | var sb = new StringBuilder();
390 | sb.Append($"{method} ");
391 | var tokenRequest = isAuthenticated && !string.IsNullOrEmpty(authenticationToken);
392 | if (tokenRequest)
393 | {
394 | sb.Append($"{authenticationToken} ");
395 | if (arguments.Length > 0)
396 | {
397 | var isStructBody = arguments.Any(x => !string.IsNullOrEmpty(x.Key));
398 | sb.Append($"");
399 | if (isStructBody)
400 | {
401 | sb.Append("");
402 | foreach (var item in arguments)
403 | {
404 | var argumentType = GetArgumentTypeName(item.Value);
405 | sb.Append($"{item.Key} <{argumentType}>{item.Value}{argumentType}> ");
406 | }
407 | sb.Append($" ");
408 | }
409 | else
410 | {
411 | foreach (var item in arguments)
412 | {
413 | var argumentType = GetArgumentTypeName(item.Value);
414 | sb.Append($"<{argumentType}>{item.Value}{argumentType}> ");
415 | }
416 | }
417 | sb.Append($" ");
418 | }
419 | }
420 | else
421 | {
422 | foreach (var item in arguments)
423 | {
424 | var argumentType = GetArgumentTypeName(item.Value);
425 | sb.Append($"<{argumentType}>{item.Value}{argumentType}> ");
426 | }
427 | }
428 | sb.AppendLine(" ");
429 | return sb.ToString();
430 | }
431 |
432 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
433 | private static KeyValuePair Arg(string key, object value)
434 | {
435 | return new KeyValuePair(key, value);
436 | }
437 |
438 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
439 | private static KeyValuePair Arg(object value)
440 | {
441 | return new KeyValuePair("", value);
442 | }
443 |
444 | private static string GetArgumentTypeName(object item)
445 | {
446 | if (item is string) return "string";
447 | if (item is double) return "double";
448 | if (item is int) return "int";
449 | return "string";
450 | }
451 |
452 | public class Subtitle
453 | {
454 | public Subtitle() { } // required for deserialization
455 | public string IdSubtitleFile { get; set; }
456 | public string SubFileName { get; set; }
457 | public string SubLanguageId { get; set; }
458 | public string LanguageName { get; set; }
459 | public string MovieReleaseName { get; set; }
460 | public string MovieName { get; set; }
461 | public string SubEncoding { get; set; }
462 | public string SubDownloadLink { get; set; }
463 | public string ZipDownloadLink { get; set; }
464 | public string SubtitleLink { get; set; }
465 | public string SubRating { get; set; }
466 |
467 | public override string ToString()
468 | {
469 | return SubFileName ?? MovieReleaseName;
470 | }
471 | }
472 |
473 | public class SubtitleData
474 | {
475 | public SubtitleData() { }
476 | public string IdSubtitleFile { get; set; }
477 | public string Data { get; set; }
478 | }
479 | }
480 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/Providers/Subscene.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.RegularExpressions;
4 | using System.Threading.Tasks;
5 | using SubSyncLib.Logic;
6 |
7 | namespace SubSyncLib.Providers
8 | {
9 | public class Subscene : SubtitleProviderBase
10 | {
11 | private const string SearchApiUrlFormat = "https://subscene.com/subtitles/release?q={0}&r=true";
12 | private const string SubtitleApiUrlFormat = "https://subscene.com/{0}";
13 |
14 | public Subscene(HashSet languages) : base(languages)
15 | {
16 | }
17 |
18 | public override async Task GetAsync(VideoFile video)//string name, string outputDirectory)
19 | {
20 | var name = video.Name;
21 | var outputDirectory = video.Directory?.FullName ?? "./";
22 | var url = await FindAsync(name);
23 | if (string.IsNullOrEmpty(url))
24 | {
25 | throw new Exception($"No subtitles for {name} could befound");
26 | }
27 |
28 | var subtitlePageContent = await DownloadStringAsync(url);
29 | foreach (var language in Languages)
30 | {
31 | var match = Regex.Match(subtitlePageContent, $@"\/subtitles\/{language}-text\/[a-zA-Z0-9_-]*");
32 | if (match.Success)
33 | {
34 | var downloadUrl = string.Format(SubtitleApiUrlFormat, match.Value);
35 | return await DownloadFileAsync(downloadUrl, outputDirectory);
36 | }
37 | }
38 |
39 | return null;
40 | }
41 |
42 | private async Task FindAsync(string name)
43 | {
44 | var searchName = GetUrlFriendlyName(name);
45 | var searchUrl = string.Format(SearchApiUrlFormat, searchName);
46 | var searchPageContent = await DownloadStringAsync(searchUrl);
47 |
48 | foreach (var language in Languages)
49 | {
50 | var match = Regex.Match(searchPageContent, $@"\/subtitles\/.*\/{language}\/[0-9]+");
51 |
52 | if (match.Success)
53 | {
54 | return string.Format(SubtitleApiUrlFormat, match.Value);
55 | }
56 | }
57 | return null;
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/SubSyncLib/SubSyncLib.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 0.1.6.2
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/SubSync.Tests/FilterTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using SubSyncLib.Logic;
3 |
4 | namespace SubSync.Tests
5 | {
6 | [TestClass]
7 | public class FilterTests
8 | {
9 | // can't be assed to name these properly xD
10 | [TestMethod]
11 | public void VideoIgnoreTest_1()
12 | {
13 | var filter = new VideoIgnoreFilter(new[] { "*.mp4" });
14 | Assert.AreEqual(true, filter.Match(@"c:\blabla\bloblo\test.mp4"));
15 | Assert.AreEqual(false, filter.Match(@"c:\blabla\bloblo\test.mp3"));
16 | }
17 |
18 | [TestMethod]
19 | public void VideoIgnoreTest_2()
20 | {
21 | var filter = new VideoIgnoreFilter(new[] { "*.mp3" });
22 | Assert.AreEqual(false, filter.Match(@"c:\blabla\bloblo\test.mp4"));
23 | Assert.AreEqual(true, filter.Match(@"c:\blabla\bloblo\test.mp3"));
24 | }
25 |
26 | [TestMethod]
27 | public void VideoIgnoreTest_3()
28 | {
29 | var filter = new VideoIgnoreFilter(new[] { "baba/*.mp3" });
30 | Assert.AreEqual(true, filter.Match(@"c:\blabla\baba\test.mp3"));
31 | Assert.AreEqual(false, filter.Match(@"c:\blabla\bloblo\test.mp3"));
32 | }
33 |
34 | [TestMethod]
35 | public void VideoIgnoreTest_4()
36 | {
37 | var filter = new VideoIgnoreFilter(new[] { "*/*.mp3" });
38 | Assert.AreEqual(true, filter.Match(@"c:\blabla\baba\test.mp3"));
39 | Assert.AreEqual(true, filter.Match(@"c:\blabla\bloblo\test.mp3"));
40 | }
41 |
42 | [TestMethod]
43 | public void VideoIgnoreTest_5()
44 | {
45 | var filter = new VideoIgnoreFilter(new[] { "*/*.*" });
46 | Assert.AreEqual(true, filter.Match(@"c:\blabla\baba\test.mp3"));
47 | Assert.AreEqual(true, filter.Match(@"c:\blabla\bloblo\test.mp3"));
48 | }
49 |
50 | [TestMethod]
51 | public void VideoIgnoreTest_6()
52 | {
53 | var filter = new VideoIgnoreFilter(new[] { "*/*" });
54 | Assert.AreEqual(true, filter.Match(@"c:\blabla\baba\test.mp3"));
55 | Assert.AreEqual(true, filter.Match(@"c:\blabla\bloblo\test.mp3"));
56 | }
57 |
58 | [TestMethod]
59 | public void VideoIgnoreTest_7()
60 | {
61 | var filter = new VideoIgnoreFilter(new[] { "*" });
62 | Assert.AreEqual(true, filter.Match(@"c:\blabla\baba\test.mp3"));
63 | Assert.AreEqual(true, filter.Match(@"c:\blabla\bloblo\test.mp3"));
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/tests/SubSync.Tests/NameDiffScoreTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using SubSyncLib.Logic;
3 |
4 | namespace SubSync.Tests
5 | {
6 | [TestClass]
7 | public class NameDiffScoreTests
8 | {
9 | [TestMethod]
10 | public void Test1()
11 | {
12 | var score0 = FilenameDiff.GetDiffScore("Greys Anatomy s11e11.mp4",
13 | "Greys.Anatomy.S09.720p.HDTV.X264-DIMENSION.mp4");
14 |
15 | Assert.AreEqual(15.6, score0);
16 |
17 | var score1 = FilenameDiff.GetDiffScore("Greys.Anatomy.S09.480p.HDTV.x264-mSD.mp4",
18 | "Greys.Anatomy.S09.720p.HDTV.X264-DIMENSION.mp4");
19 |
20 | Assert.AreEqual(6.5, score1);
21 |
22 | var score2 = FilenameDiff.GetDiffScore("Greys.Anatomy.S09.480p.HDTV.x264-mSD.mp4",
23 | "Greys.Anatomy.S09.480p.HDTV.x264-mSD.mp4");
24 |
25 | Assert.AreEqual(0, score2);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/tests/SubSync.Tests/SubSync.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 | false
7 |
8 |
9 |
10 | false
11 |
12 |
13 |
14 |
15 | false
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------