├── .editorconfig
├── .gitattributes
├── .gitignore
├── README.md
├── Sprout Downloader.sln
└── Sprout Downloader
├── App.config
├── Downloader.cs
├── Json
├── JsonContext.cs
├── M3UParser.cs
└── SproutData.cs
├── Main.Designer.cs
├── Main.cs
├── Main.resx
├── PasswordInput.Designer.cs
├── PasswordInput.cs
├── PasswordInput.resx
├── Program.cs
├── Properties
├── AssemblyInfo.cs
├── Resources.Designer.cs
├── Settings.Designer.cs
└── Settings.settings
├── QualitySelector.Designer.cs
├── QualitySelector.cs
├── QualitySelector.resx
├── Sprout Downloader.csproj
├── UI
├── AntiAliasingLabel.cs
├── RadioListBox.cs
└── TextProgressBar.cs
└── Util
├── Extensions.cs
├── ParsedURL.cs
└── Utils.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # CA1416: Проверка совместимости платформы
4 | dotnet_diagnostic.CA1416.severity = none
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.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 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Requirements:
2 | * .NET 6.0
3 | * ffmpeg(if you wanna convert *.ts files into *.mp4)
4 |
5 | ## Usage:
6 | * Download exe from releases
7 | * Run it and follow the instructions
8 |
9 | ## Also:
10 | You can use script on python 3.7+: https://github.com/DumbCodeGenerator/sprout_downloader_python
11 |
--------------------------------------------------------------------------------
/Sprout Downloader.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31912.275
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sprout Downloader", "Sprout Downloader\Sprout Downloader.csproj", "{65D86118-7EC4-429E-B0D5-CEA11650B876}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5D83A05E-25BA-4376-B5DC-CE82A76B7DF4}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | EndProjectSection
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Release|Any CPU = Release|Any CPU
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {65D86118-7EC4-429E-B0D5-CEA11650B876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {65D86118-7EC4-429E-B0D5-CEA11650B876}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {65D86118-7EC4-429E-B0D5-CEA11650B876}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {65D86118-7EC4-429E-B0D5-CEA11650B876}.Release|Any CPU.Build.0 = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {72BB6C1C-93F1-4F30-A7C4-B8655E8A7D66}
29 | EndGlobalSection
30 | EndGlobal
31 |
--------------------------------------------------------------------------------
/Sprout Downloader/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 4
16 |
17 |
18 | False
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Sprout Downloader/Downloader.cs:
--------------------------------------------------------------------------------
1 | using Sprout_Downloader.Json;
2 | using System.Collections.Concurrent;
3 | using System.Threading.Tasks.Dataflow;
4 |
5 | namespace Sprout_Downloader
6 | {
7 | public class Downloader
8 | {
9 | private readonly ConcurrentDictionary _progresses = new();
10 | private int _current;
11 | private long _fullSize;
12 | private Action, int, int, long> _progressCallback;
13 | private int _total;
14 |
15 | public async Task Start(M3UParser parser, Action, int, int, long> progressCallback,
16 | Action finishCallback)
17 | {
18 | PlaylistParser playlist = parser.GetPlaylistParser();
19 | _fullSize = parser.GetInaccurateVideoSize();
20 | _total = playlist.GetSegmentsCount();
21 | _progressCallback = progressCallback;
22 |
23 | ActionBlock workers = new ActionBlock(segment =>
24 | {
25 | try
26 | {
27 | Download(segment).Wait(Program.GetCancellationToken());
28 | }
29 | catch (Exception)
30 | {
31 | }
32 | }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = Properties.Settings.Default.threadCount });
33 |
34 | playlist.GetSegments().ForEach(x => workers.Post(x));
35 | workers.Complete();
36 | await workers.Completion;
37 |
38 | finishCallback(parser);
39 | }
40 |
41 | private void DownloadProgressCallback(string filename, Status status)
42 | {
43 | _progresses.AddOrUpdate(filename, status, (key, oldValue) => status);
44 |
45 | _progressCallback(_progresses.Values, _current, _total, _fullSize);
46 | }
47 |
48 | private void DownloadCompleteCallback()
49 | {
50 | Interlocked.Increment(ref _current);
51 | }
52 |
53 | private async Task Download(Segment segment)
54 | {
55 | using HttpClient hc = new();
56 | using HttpResponseMessage response = await hc.GetAsync(segment.Url, Program.GetCancellationToken());
57 | response.EnsureSuccessStatusCode();
58 | long fileSize = response.Content.Headers.ContentLength ?? -1L;
59 | int bufferSize = 4 * 1024 * 1024;
60 |
61 | Directory.CreateDirectory(segment.Folder);
62 |
63 | using Stream content = await response.Content.ReadAsStreamAsync(Program.GetCancellationToken());
64 | using FileStream fileStream = new FileStream(segment.GetFullPath(), FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, true);
65 |
66 | long totalRead = 0L;
67 | byte[] buffer = new byte[bufferSize];
68 | bool isMoreToRead = true;
69 |
70 | do
71 | {
72 | int read = await content.ReadAsync(buffer.AsMemory(0, buffer.Length), Program.GetCancellationToken());
73 | if (read == 0)
74 | {
75 | isMoreToRead = false;
76 | DownloadCompleteCallback();
77 | }
78 | else
79 | {
80 | await fileStream.WriteAsync(buffer.AsMemory(0, read), Program.GetCancellationToken());
81 |
82 | totalRead += read;
83 | DownloadProgressCallback(segment.GetFullPath(), new Status { BytesReceived = totalRead, TotalBytes = fileSize });
84 | }
85 | }
86 | while (isMoreToRead);
87 | }
88 |
89 | public class Status
90 | {
91 | public long BytesReceived { get; set; }
92 | public long TotalBytes { get; set; }
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/Sprout Downloader/Json/JsonContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Sprout_Downloader.Json
4 | {
5 | [JsonSerializable(typeof(SproutData))]
6 | internal partial class JsonContext : JsonSerializerContext
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sprout Downloader/Json/M3UParser.cs:
--------------------------------------------------------------------------------
1 | using Sprout_Downloader.Util;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace Sprout_Downloader.Json
5 | {
6 | public class M3UParser
7 | {
8 | private readonly SproutData _dataObj;
9 |
10 | private readonly Lazy> _lazyPlaylists;
11 | private PlaylistParser _playlistParser;
12 |
13 | public M3UParser(string input, SproutData dataObj)
14 | {
15 | _dataObj = dataObj;
16 |
17 | _lazyPlaylists = new Lazy>(() =>
18 | {
19 | MatchCollection matchList = Regex.Matches(input,
20 | @"^#EXT-X-STREAM-INF:.*?BANDWIDTH=(.*?),RESOLUTION.*$\n^(.*?\.m3u8)$", RegexOptions.Multiline);
21 | return matchList.Cast().Select(match => new Playlist
22 | {
23 | Quality = match.Groups[2].Value.Replace(".m3u8", "p"),
24 | Url = _dataObj.SignUrl(_dataObj.GetBaseUrl() + match.Groups[2].Value),
25 | InaccurateSize = long.Parse(match.Groups[1].Value) / 8 * (long)_dataObj.Duration
26 | });
27 | });
28 | }
29 |
30 | public int Index { get; set; }
31 |
32 | private IEnumerable GetPlaylists()
33 | {
34 | return _lazyPlaylists.Value;
35 | }
36 |
37 | public IEnumerable GetQualityList()
38 | {
39 | return GetPlaylists().Select(x => x.Quality);
40 | }
41 |
42 | private int GetBestQuality()
43 | {
44 | return GetPlaylists().Count() - 1;
45 | }
46 |
47 | public string GetPlaylistUrl()
48 | {
49 | return GetPlaylists().ElementAt(Properties.Settings.Default.bestQuality ? GetBestQuality() : Index).Url;
50 | }
51 |
52 | public long GetInaccurateVideoSize()
53 | {
54 | return GetPlaylists().ElementAt(Properties.Settings.Default.bestQuality ? GetBestQuality() : Index).InaccurateSize;
55 | }
56 |
57 | public void SetPlaylistString(string input)
58 | {
59 | _playlistParser = new PlaylistParser(input, _dataObj, this);
60 | }
61 |
62 | public PlaylistParser GetPlaylistParser()
63 | {
64 | return _playlistParser;
65 | }
66 |
67 | public string GetVideoTitle()
68 | {
69 | return Path.GetFileNameWithoutExtension(_dataObj.Title);
70 | }
71 |
72 | public string GetSegmentsFolder()
73 | {
74 | return "segments_" + GetVideoTitle();
75 | }
76 | }
77 |
78 | public class PlaylistParser
79 | {
80 | private readonly Lazy _lazyKey;
81 | private readonly Lazy> _lazySegments;
82 |
83 | public PlaylistParser(string m3UString, SproutData dataObj, M3UParser parser)
84 | {
85 | _lazyKey = new Lazy(() =>
86 | {
87 | Match match = Regex.Match(m3UString, "#EXT-X-KEY:.*?URI=\"(.*?)\".*?IV=0x(.*?)$", RegexOptions.Multiline);
88 | using HttpClient hc = new();
89 | byte[] bytes = hc.GetByteArrayAsync(dataObj.SignUrl(dataObj.GetBaseUrl() + match.Groups[1].Value)).Result;
90 | return new Key
91 | {
92 | Iv = Utils.StringToByteArrayFastest(match.Groups[2].Value),
93 | Bytes = bytes
94 | };
95 | });
96 |
97 | _lazySegments = new Lazy>(() =>
98 | {
99 | MatchCollection matchList = Regex.Matches(m3UString, "^.*?\\.ts$", RegexOptions.Multiline);
100 | return matchList.Cast().Select(match => new Segment
101 | {
102 | Folder = parser.GetSegmentsFolder(),
103 | Filename = match.Value,
104 | Url = dataObj.SignUrl(dataObj.GetBaseUrl() + match.Value)
105 | }).ToList();
106 | });
107 | }
108 |
109 | public Key GetKey()
110 | {
111 | return _lazyKey.Value;
112 | }
113 |
114 | public List GetSegments()
115 | {
116 | return _lazySegments.Value;
117 | }
118 |
119 | public int GetSegmentsCount()
120 | {
121 | return GetSegments().Count;
122 | }
123 | }
124 |
125 | public class Playlist
126 | {
127 | public string Quality { get; set; }
128 | public long InaccurateSize { get; set; }
129 | public string Url { get; set; }
130 | }
131 |
132 | public class Key
133 | {
134 | public byte[] Bytes { get; set; }
135 | public byte[] Iv { get; set; }
136 | }
137 |
138 | public class Segment
139 | {
140 | public string Folder { get; set; }
141 | public string Filename { get; set; }
142 | public string Url { get; set; }
143 |
144 | public string GetFullPath()
145 | {
146 | return Folder + "/" + Filename;
147 | }
148 | }
149 | }
--------------------------------------------------------------------------------
/Sprout Downloader/Json/SproutData.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Sprout_Downloader.Json
4 | {
5 | public class SproutData
6 | {
7 | [JsonPropertyName("title")] public string Title { get; set; }
8 |
9 | [JsonPropertyName("duration")] public double Duration { get; set; }
10 |
11 | [JsonPropertyName("sessionID")] public string SessionId { get; set; }
12 |
13 | [JsonPropertyName("s3_user_hash")] public string UserHash { get; set; }
14 |
15 | [JsonPropertyName("s3_video_hash")] public string VideoHash { get; set; }
16 |
17 | [JsonPropertyName("signatures")] public Dictionary Signatures { get; set; }
18 |
19 | public string SignUrl(string url)
20 | {
21 | if (url.EndsWith(".m3u8"))
22 | return url + Signatures["m"] + SessionId;
23 | if (url.EndsWith(".key"))
24 | return url + Signatures["k"] + SessionId;
25 | if (url.EndsWith(".ts"))
26 | return url + Signatures["t"] + SessionId;
27 | return "Wrong url type!";
28 | }
29 |
30 | public string GetBaseUrl()
31 | {
32 | return "https://hls2.videos.sproutvideo.com/" + UserHash + '/' + VideoHash + "/video/";
33 | }
34 |
35 | public string GetIndexUrl()
36 | {
37 | return SignUrl(GetBaseUrl() + "index.m3u8");
38 | }
39 | }
40 |
41 | public class Signature
42 | {
43 | [JsonPropertyName("CloudFront-Policy")] public string Policy { get; set; }
44 |
45 | [JsonPropertyName("CloudFront-Signature")] public string SignatureValue { get; set; }
46 |
47 | [JsonPropertyName("CloudFront-Key-Pair-Id")]
48 | public string KeyIdPair { get; set; }
49 |
50 | public override string ToString()
51 | {
52 | return "?Policy=" + Policy + "&Signature=" + SignatureValue + "&Key-Pair-Id=" + KeyIdPair +
53 | "&sessionID=";
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/Sprout Downloader/Main.Designer.cs:
--------------------------------------------------------------------------------
1 | using Sprout_Downloader.UI;
2 |
3 | namespace Sprout_Downloader
4 | {
5 | partial class Main
6 | {
7 | ///
8 | /// Обязательная переменная конструктора.
9 | ///
10 | private System.ComponentModel.IContainer components = null;
11 |
12 | ///
13 | /// Освободить все используемые ресурсы.
14 | ///
15 | /// истинно, если управляемый ресурс должен быть удален; иначе ложно.
16 | protected override void Dispose(bool disposing)
17 | {
18 | if (disposing && (components != null))
19 | {
20 | components.Dispose();
21 | }
22 | base.Dispose(disposing);
23 | }
24 |
25 | #region Код, автоматически созданный конструктором форм Windows
26 |
27 | ///
28 | /// Требуемый метод для поддержки конструктора — не изменяйте
29 | /// содержимое этого метода с помощью редактора кода.
30 | ///
31 | private void InitializeComponent()
32 | {
33 | this.urlTextBox = new System.Windows.Forms.TextBox();
34 | this.button1 = new System.Windows.Forms.Button();
35 | this.panel1 = new System.Windows.Forms.Panel();
36 | this.totalProgressLabel = new Sprout_Downloader.UI.AntiAliasingLabel();
37 | this.downloadProgress = new Sprout_Downloader.UI.TextProgressBar();
38 | this.actionLabel = new Sprout_Downloader.UI.AntiAliasingLabel();
39 | this.label1 = new Sprout_Downloader.UI.AntiAliasingLabel();
40 | this.menuStrip1 = new System.Windows.Forms.MenuStrip();
41 | this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
42 | this.threadsCountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
43 | this.toolStripTextBox1 = new System.Windows.Forms.ToolStripTextBox();
44 | this.qualityToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
45 | this.alwaysAskToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
46 | this.alwaysTheBestToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
47 | this.panel1.SuspendLayout();
48 | this.menuStrip1.SuspendLayout();
49 | this.SuspendLayout();
50 | //
51 | // urlTextBox
52 | //
53 | this.urlTextBox.AcceptsReturn = true;
54 | this.urlTextBox.Location = new System.Drawing.Point(14, 73);
55 | this.urlTextBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
56 | this.urlTextBox.MaxLength = 99999;
57 | this.urlTextBox.Multiline = true;
58 | this.urlTextBox.Name = "urlTextBox";
59 | this.urlTextBox.PlaceholderText = "URL Syntax: ||";
61 | this.urlTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both;
62 | this.urlTextBox.Size = new System.Drawing.Size(542, 305);
63 | this.urlTextBox.TabIndex = 0;
64 | this.urlTextBox.WordWrap = false;
65 | //
66 | // button1
67 | //
68 | this.button1.Font = new System.Drawing.Font("Samsung Sans", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
69 | this.button1.Location = new System.Drawing.Point(14, 384);
70 | this.button1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
71 | this.button1.Name = "button1";
72 | this.button1.Size = new System.Drawing.Size(543, 52);
73 | this.button1.TabIndex = 2;
74 | this.button1.Text = "Download";
75 | this.button1.UseVisualStyleBackColor = true;
76 | this.button1.Click += new System.EventHandler(this.button1_ClickAsync);
77 | //
78 | // panel1
79 | //
80 | this.panel1.Controls.Add(this.totalProgressLabel);
81 | this.panel1.Controls.Add(this.downloadProgress);
82 | this.panel1.Controls.Add(this.actionLabel);
83 | this.panel1.Location = new System.Drawing.Point(14, 455);
84 | this.panel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
85 | this.panel1.Name = "panel1";
86 | this.panel1.Size = new System.Drawing.Size(542, 164);
87 | this.panel1.TabIndex = 3;
88 | //
89 | // totalProgressLabel
90 | //
91 | this.totalProgressLabel.Font = new System.Drawing.Font("Samsung Sans", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
92 | this.totalProgressLabel.Location = new System.Drawing.Point(3, 130);
93 | this.totalProgressLabel.Name = "totalProgressLabel";
94 | this.totalProgressLabel.Size = new System.Drawing.Size(536, 32);
95 | this.totalProgressLabel.TabIndex = 6;
96 | this.totalProgressLabel.Text = "Downloaded videos: 0/0";
97 | this.totalProgressLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
98 | this.totalProgressLabel.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
99 | //
100 | // downloadProgress
101 | //
102 | this.downloadProgress.CustomText = "";
103 | this.downloadProgress.Location = new System.Drawing.Point(0, 75);
104 | this.downloadProgress.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
105 | this.downloadProgress.Name = "downloadProgress";
106 | this.downloadProgress.ProgressColor = System.Drawing.Color.LightGreen;
107 | this.downloadProgress.Size = new System.Drawing.Size(542, 48);
108 | this.downloadProgress.TabIndex = 5;
109 | this.downloadProgress.TextColor = System.Drawing.Color.Black;
110 | this.downloadProgress.TextFont = new System.Drawing.Font("Samsung Sans", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
111 | this.downloadProgress.VisualMode = Sprout_Downloader.UI.ProgressBarDisplayMode.CustomText;
112 | //
113 | // actionLabel
114 | //
115 | this.actionLabel.Font = new System.Drawing.Font("Samsung Sans", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
116 | this.actionLabel.Location = new System.Drawing.Point(0, 0);
117 | this.actionLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
118 | this.actionLabel.Name = "actionLabel";
119 | this.actionLabel.Padding = new System.Windows.Forms.Padding(12);
120 | this.actionLabel.Size = new System.Drawing.Size(542, 72);
121 | this.actionLabel.TabIndex = 4;
122 | this.actionLabel.Text = "Action...";
123 | this.actionLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
124 | this.actionLabel.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
125 | //
126 | // label1
127 | //
128 | this.label1.Font = new System.Drawing.Font("Samsung Sans", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
129 | this.label1.Location = new System.Drawing.Point(14, 42);
130 | this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
131 | this.label1.Name = "label1";
132 | this.label1.Size = new System.Drawing.Size(542, 27);
133 | this.label1.TabIndex = 1;
134 | this.label1.Text = "Insert Video URLs:";
135 | this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
136 | this.label1.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
137 | //
138 | // menuStrip1
139 | //
140 | this.menuStrip1.BackColor = System.Drawing.SystemColors.Control;
141 | this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
142 | this.settingsToolStripMenuItem});
143 | this.menuStrip1.Location = new System.Drawing.Point(0, 0);
144 | this.menuStrip1.Name = "menuStrip1";
145 | this.menuStrip1.Size = new System.Drawing.Size(570, 24);
146 | this.menuStrip1.TabIndex = 4;
147 | this.menuStrip1.Text = "menuStrip1";
148 | //
149 | // settingsToolStripMenuItem
150 | //
151 | this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
152 | this.threadsCountToolStripMenuItem,
153 | this.qualityToolStripMenuItem});
154 | this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
155 | this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
156 | this.settingsToolStripMenuItem.Text = "Settings";
157 | //
158 | // threadsCountToolStripMenuItem
159 | //
160 | this.threadsCountToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
161 | this.toolStripTextBox1});
162 | this.threadsCountToolStripMenuItem.Name = "threadsCountToolStripMenuItem";
163 | this.threadsCountToolStripMenuItem.Size = new System.Drawing.Size(151, 22);
164 | this.threadsCountToolStripMenuItem.Text = "Threads Count";
165 | //
166 | // toolStripTextBox1
167 | //
168 | this.toolStripTextBox1.Name = "toolStripTextBox1";
169 | this.toolStripTextBox1.Size = new System.Drawing.Size(100, 23);
170 | this.toolStripTextBox1.Text = "4";
171 | this.toolStripTextBox1.TextChanged += new System.EventHandler(this.toolStripTextBox1_TextChanged);
172 | //
173 | // qualityToolStripMenuItem
174 | //
175 | this.qualityToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
176 | this.alwaysAskToolStripMenuItem,
177 | this.alwaysTheBestToolStripMenuItem});
178 | this.qualityToolStripMenuItem.Name = "qualityToolStripMenuItem";
179 | this.qualityToolStripMenuItem.Size = new System.Drawing.Size(151, 22);
180 | this.qualityToolStripMenuItem.Text = "Quality";
181 | //
182 | // alwaysAskToolStripMenuItem
183 | //
184 | this.alwaysAskToolStripMenuItem.Name = "alwaysAskToolStripMenuItem";
185 | this.alwaysAskToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
186 | this.alwaysAskToolStripMenuItem.Text = "Always ask";
187 | this.alwaysAskToolStripMenuItem.Click += new System.EventHandler(this.alwaysAskToolStripMenuItem_Click);
188 | //
189 | // alwaysTheBestToolStripMenuItem
190 | //
191 | this.alwaysTheBestToolStripMenuItem.Name = "alwaysTheBestToolStripMenuItem";
192 | this.alwaysTheBestToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
193 | this.alwaysTheBestToolStripMenuItem.Text = "Always the best";
194 | this.alwaysTheBestToolStripMenuItem.Click += new System.EventHandler(this.alwaysTheBestToolStripMenuItem_Click);
195 | //
196 | // Main
197 | //
198 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
199 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
200 | this.ClientSize = new System.Drawing.Size(570, 626);
201 | this.Controls.Add(this.panel1);
202 | this.Controls.Add(this.button1);
203 | this.Controls.Add(this.label1);
204 | this.Controls.Add(this.urlTextBox);
205 | this.Controls.Add(this.menuStrip1);
206 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
207 | this.MainMenuStrip = this.menuStrip1;
208 | this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
209 | this.MaximizeBox = false;
210 | this.Name = "Main";
211 | this.Text = "Sprout Downloader (by JCTrich)";
212 | this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Main_FormClosing);
213 | this.Load += new System.EventHandler(this.Form1_Load);
214 | this.panel1.ResumeLayout(false);
215 | this.menuStrip1.ResumeLayout(false);
216 | this.menuStrip1.PerformLayout();
217 | this.ResumeLayout(false);
218 | this.PerformLayout();
219 |
220 | }
221 |
222 | #endregion
223 |
224 | private System.Windows.Forms.TextBox urlTextBox;
225 | private System.Windows.Forms.Button button1;
226 | private System.Windows.Forms.Panel panel1;
227 | private TextProgressBar downloadProgress;
228 | private AntiAliasingLabel label1;
229 | private AntiAliasingLabel actionLabel;
230 | private MenuStrip menuStrip1;
231 | private ToolStripMenuItem settingsToolStripMenuItem;
232 | private ToolStripMenuItem threadsCountToolStripMenuItem;
233 | private ToolStripTextBox toolStripTextBox1;
234 | private ToolStripMenuItem qualityToolStripMenuItem;
235 | private ToolStripMenuItem alwaysAskToolStripMenuItem;
236 | private ToolStripMenuItem alwaysTheBestToolStripMenuItem;
237 | private AntiAliasingLabel totalProgressLabel;
238 | }
239 | }
240 |
241 |
--------------------------------------------------------------------------------
/Sprout Downloader/Main.cs:
--------------------------------------------------------------------------------
1 | using Sprout_Downloader.Json;
2 | using Sprout_Downloader.Util;
3 | using System.Net;
4 | using System.Text;
5 | using System.Text.Json;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace Sprout_Downloader
9 | {
10 | public partial class Main : Form
11 | {
12 | private const int OrigHeight = 492;
13 | private const int InfoHeight = 565;
14 | private const int FullHeight = 665;
15 |
16 | private static readonly string[] SizeSuffixes =
17 | {"bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
18 |
19 | private static readonly string _totalProgTemp = "Downloaded videos: %current/%total";
20 |
21 | private int _currentProg = 0;
22 |
23 | private static readonly CookieContainer Cookies = new();
24 |
25 | private static readonly HttpClient
26 | WebClient = new(new HttpClientHandler { CookieContainer = Cookies });
27 |
28 | private List _videoUrls;
29 |
30 | private bool _stopButton = false;
31 |
32 |
33 | public Main()
34 | {
35 | InitializeComponent();
36 | }
37 |
38 | private async void button1_ClickAsync(object sender, EventArgs e)
39 | {
40 | if (_stopButton)
41 | {
42 | Program.Cancel();
43 | ResetState();
44 | return;
45 | }
46 |
47 | Program.RefreshCTS();
48 |
49 | SetStop(true);
50 |
51 | downloadProgress.CustomText = string.Empty;
52 |
53 | _videoUrls = urlTextBox.ParseURL();
54 |
55 | Height = OrigHeight;
56 | if (!_videoUrls.Any())
57 | {
58 | ShowError("Couldn't parse not a single URL");
59 | return;
60 | }
61 |
62 | Height = FullHeight;
63 | UpdateTotalProg();
64 |
65 | foreach (ParsedURL videoUrl in _videoUrls)
66 | {
67 | if (Program.IsCancellationRequested())
68 | break;
69 |
70 | SetAction($"Getting link #{_currentProg + 1}...");
71 | downloadProgress.Value = 0;
72 | await ProcessLink(videoUrl);
73 | }
74 | }
75 |
76 | private async Task ProcessLink(ParsedURL item)
77 | {
78 | if (Program.IsCancellationRequested())
79 | return;
80 |
81 | using HttpResponseMessage response = await WebClient.GetAsync(item.URL, Program.GetCancellationToken());
82 | if (response == null)
83 | {
84 | ShowError("Empty response!");
85 | return;
86 | }
87 |
88 | string responseString = await response.Content.ReadAsStringAsync(Program.GetCancellationToken());
89 |
90 | if (response.StatusCode != HttpStatusCode.OK && responseString.Contains("Password Protected Video",
91 | StringComparison.OrdinalIgnoreCase))
92 | {
93 | string authToken = Regex.Match(responseString, "name='authenticity_token' value='(.*?)'").Groups[1]
94 | .Value;
95 |
96 | if (string.IsNullOrWhiteSpace(authToken))
97 | ShowError("Can't get auth token from the page");
98 | else
99 | await CheckIfNeedInput(authToken, item);
100 | }
101 | else if (response.StatusCode != HttpStatusCode.OK)
102 | {
103 | ShowError($"Can't get the link. Status Code: {response.StatusCode}");
104 | }
105 | else
106 | {
107 | await DownloadVideo(responseString, item);
108 | }
109 | }
110 |
111 | private async Task CheckIfNeedInput(string authToken, ParsedURL item)
112 | {
113 | if (Program.IsCancellationRequested())
114 | return;
115 |
116 | if (item.Password == null)
117 | {
118 | using PasswordInput passDialog = new(item.URL);
119 | if (passDialog.ShowDialog(this) == DialogResult.OK)
120 | await PutPassword(authToken, item.SetPassword(passDialog.GetPassword()));
121 | else
122 | ShowError("Password input cancelled");
123 | }
124 | else
125 | {
126 | await PutPassword(authToken, item);
127 | }
128 | }
129 |
130 | private async Task PutPassword(string authToken, ParsedURL item)
131 | {
132 | if (Program.IsCancellationRequested())
133 | return;
134 |
135 | Dictionary values = new Dictionary()
136 | {
137 | {"password", item.Password},
138 | {"authenticity_token", authToken},
139 | {"_method", "put"}
140 | };
141 |
142 | FormUrlEncodedContent content = new FormUrlEncodedContent(values);
143 |
144 | using HttpResponseMessage response = await WebClient.PostAsync(item.URL, content, Program.GetCancellationToken());
145 |
146 | if (response.IsSuccessStatusCode)
147 | {
148 | string responseString = await response.Content.ReadAsStringAsync(Program.GetCancellationToken());
149 | string embeddedUrl = Regex.Match(responseString, "