├── .gitattributes
├── .github
└── workflows
│ ├── main.yml
│ └── publish-release.yml
├── .gitignore
├── Changelog.md
├── LICENSE.txt
├── M3u8Downloader_H.555dd7.plugin
├── Class1.cs
├── M3u8Downloader_H.555dd7.plugin.csproj
├── Models
│ ├── PlayerInfo.cs
│ ├── PlayerRequest.cs
│ └── Response.cs
├── Streams
│ ├── Extractor.cs
│ ├── StreamClient.cs
│ └── VideoId.cs
└── Utils
│ ├── StringExtensions.cs
│ └── WebSocketClient.cs
├── M3u8Downloader_H.Common
├── Extensions
│ ├── ConvertExtensions.cs
│ ├── CryptExtensions.cs
│ ├── GenericExtensions.cs
│ ├── HttpClientExtension.cs
│ ├── HttpRequestMessageExtension.cs
│ └── StringExtensions.cs
├── M3u8Downloader_H.Common.csproj
├── M3u8Infos
│ ├── M3UFileInfo.cs
│ ├── M3UKeyInfo.cs
│ ├── M3UMediaInfo.cs
│ └── M3UStreamInfo.cs
└── Utils
│ ├── JsonPathConverter.cs
│ ├── KV.cs
│ └── To.cs
├── M3u8Downloader_H.Plugin.Abstractions
├── IAttributeReader.cs
├── IAttributeReaderManager.cs
├── IDownloadService.cs
├── IM3u8FileInfoStreamService.cs
├── IM3u8UriProvider.cs
├── IM3uFileReader.cs
├── IPluginBuilder.cs
└── M3u8Downloader_H.Plugin.Abstractions.csproj
├── M3u8Downloader_H.Plugins.sln
├── M3u8Downloader_H.bilibili.plugin
├── Class1.cs
├── Extensions
│ └── StringExtension.cs
├── Lives
│ ├── LiveClient.cs
│ └── LiveId.cs
├── M3u8Downloader_H.bilibili.plugin.csproj
├── Models
│ └── LiveRoomInfos.cs
└── Readers
│ └── MediaAttributeReader.cs
├── M3u8Downloader_H.douyin.plugin
├── Class1.cs
├── Lives
│ ├── LiveClient.cs
│ └── LiveId.cs
├── M3u8Downloader_H.douyin.plugin.csproj
└── Models
│ └── StreamInfos.cs
├── M3u8Downloader_H.vlive.plugin
├── Class1.cs
├── M3u8Downloader_H.vlive.plugin.csproj
├── Models
│ ├── InkeyInfos.cs
│ ├── PostDetail.cs
│ ├── StreamInfos.cs
│ └── VodPlayInfo.cs
├── Readers
│ └── M3UFileReader.cs
└── Streams
│ ├── StreamClient.cs
│ └── VideoId.cs
├── README.md
├── buildall.sh
└── datasets.json
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - "*"
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | env:
15 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
16 | DOTNET_NOLOGO: true
17 | DOTNET_CLI_TELEMETRY_OPTOUT: true
18 |
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v3
23 |
24 | - name: Install .NET
25 | uses: actions/setup-dotnet@v3
26 | with:
27 | dotnet-version: 6.0.x
28 |
29 | - name: Publish
30 | run: |
31 | mkdir release
32 | chmod 755 buildall.sh
33 | ./buildall.sh release
34 |
35 | - name: Create release
36 | if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
37 | uses: softprops/action-gh-release@v1
38 | with:
39 | body: |
40 | - [更新日志](https://github.com/Harlan-H/M3u8Downloader_H.Plugins/blob/master/Changelog.md)
41 | draft: false
42 | prerelease: false
43 | files: release/*.zip
44 |
45 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: publish-release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: '版本号'
8 | required: true
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | env:
15 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
16 | DOTNET_NOLOGO: true
17 | DOTNET_CLI_TELEMETRY_OPTOUT: true
18 |
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v3
23 |
24 | - name: Install .NET
25 | uses: actions/setup-dotnet@v3
26 | with:
27 | dotnet-version: 6.0.x
28 |
29 | - name: Publish
30 | run: |
31 | mkdir release
32 | chmod 755 buildall.sh
33 | ./buildall.sh release
34 |
35 | - name: Create release
36 | uses: softprops/action-gh-release@v1
37 | with:
38 | body: |
39 | - [更新日志](https://github.com/Harlan-H/M3u8Downloader_H.Plugins/blob/master/Changelog.md)
40 | draft: false
41 | prerelease: false
42 | name: ${{ github.event.inputs.version }}
43 | tag_name: ${{ github.event.inputs.version }}
44 | files: release/*.zip
45 |
46 |
--------------------------------------------------------------------------------
/.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 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 | [Mm]3u8Downloader_H.Test
36 |
37 | # Visual Studio 2015/2017 cache/options directory
38 | .vs/
39 | # Uncomment if you have tasks that create the project's static files in wwwroot
40 | #wwwroot/
41 |
42 | # Visual Studio 2017 auto generated files
43 | Generated\ Files/
44 |
45 | # MSTest test Results
46 | [Tt]est[Rr]esult*/
47 | [Bb]uild[Ll]og.*
48 |
49 | # NUnit
50 | *.VisualState.xml
51 | TestResult.xml
52 | nunit-*.xml
53 |
54 | # Build Results of an ATL Project
55 | [Dd]ebugPS/
56 | [Rr]eleasePS/
57 | dlldata.c
58 |
59 | # Benchmark Results
60 | BenchmarkDotNet.Artifacts/
61 |
62 | # .NET Core
63 | project.lock.json
64 | project.fragment.lock.json
65 | artifacts/
66 |
67 | # ASP.NET Scaffolding
68 | ScaffoldingReadMe.txt
69 |
70 | # StyleCop
71 | StyleCopReport.xml
72 |
73 | # Files built by Visual Studio
74 | *_i.c
75 | *_p.c
76 | *_h.h
77 | *.ilk
78 | *.meta
79 | *.obj
80 | *.iobj
81 | *.pch
82 | *.pdb
83 | *.ipdb
84 | *.pgc
85 | *.pgd
86 | *.rsp
87 | *.sbr
88 | *.tlb
89 | *.tli
90 | *.tlh
91 | *.tmp
92 | *.tmp_proj
93 | *_wpftmp.csproj
94 | *.log
95 | *.vspscc
96 | *.vssscc
97 | .builds
98 | *.pidb
99 | *.svclog
100 | *.scc
101 |
102 | # Chutzpah Test files
103 | _Chutzpah*
104 |
105 | # Visual C++ cache files
106 | ipch/
107 | *.aps
108 | *.ncb
109 | *.opendb
110 | *.opensdf
111 | *.sdf
112 | *.cachefile
113 | *.VC.db
114 | *.VC.VC.opendb
115 |
116 | # Visual Studio profiler
117 | *.psess
118 | *.vsp
119 | *.vspx
120 | *.sap
121 |
122 | # Visual Studio Trace Files
123 | *.e2e
124 |
125 | # TFS 2012 Local Workspace
126 | $tf/
127 |
128 | # Guidance Automation Toolkit
129 | *.gpState
130 |
131 | # ReSharper is a .NET coding add-in
132 | _ReSharper*/
133 | *.[Rr]e[Ss]harper
134 | *.DotSettings.user
135 |
136 | # TeamCity is a build add-in
137 | _TeamCity*
138 |
139 | # DotCover is a Code Coverage Tool
140 | *.dotCover
141 |
142 | # AxoCover is a Code Coverage Tool
143 | .axoCover/*
144 | !.axoCover/settings.json
145 |
146 | # Coverlet is a free, cross platform Code Coverage Tool
147 | coverage*.json
148 | coverage*.xml
149 | coverage*.info
150 |
151 | # Visual Studio code coverage results
152 | *.coverage
153 | *.coveragexml
154 |
155 | # NCrunch
156 | _NCrunch_*
157 | .*crunch*.local.xml
158 | nCrunchTemp_*
159 |
160 | # MightyMoose
161 | *.mm.*
162 | AutoTest.Net/
163 |
164 | # Web workbench (sass)
165 | .sass-cache/
166 |
167 | # Installshield output folder
168 | [Ee]xpress/
169 |
170 | # DocProject is a documentation generator add-in
171 | DocProject/buildhelp/
172 | DocProject/Help/*.HxT
173 | DocProject/Help/*.HxC
174 | DocProject/Help/*.hhc
175 | DocProject/Help/*.hhk
176 | DocProject/Help/*.hhp
177 | DocProject/Help/Html2
178 | DocProject/Help/html
179 |
180 | # Click-Once directory
181 | publish/
182 |
183 | # Publish Web Output
184 | *.[Pp]ublish.xml
185 | *.azurePubxml
186 | # Note: Comment the next line if you want to checkin your web deploy settings,
187 | # but database connection strings (with potential passwords) will be unencrypted
188 | *.pubxml
189 | *.publishproj
190 |
191 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
192 | # checkin your Azure Web App publish settings, but sensitive information contained
193 | # in these scripts will be unencrypted
194 | PublishScripts/
195 |
196 | # NuGet Packages
197 | *.nupkg
198 | # NuGet Symbol Packages
199 | *.snupkg
200 | # The packages folder can be ignored because of Package Restore
201 | **/[Pp]ackages/*
202 | # except build/, which is used as an MSBuild target.
203 | !**/[Pp]ackages/build/
204 | # Uncomment if necessary however generally it will be regenerated when needed
205 | #!**/[Pp]ackages/repositories.config
206 | # NuGet v3's project.json files produces more ignorable files
207 | *.nuget.props
208 | *.nuget.targets
209 |
210 | # Microsoft Azure Build Output
211 | csx/
212 | *.build.csdef
213 |
214 | # Microsoft Azure Emulator
215 | ecf/
216 | rcf/
217 |
218 | # Windows Store app package directories and files
219 | AppPackages/
220 | BundleArtifacts/
221 | Package.StoreAssociation.xml
222 | _pkginfo.txt
223 | *.appx
224 | *.appxbundle
225 | *.appxupload
226 |
227 | # Visual Studio cache files
228 | # files ending in .cache can be ignored
229 | *.[Cc]ache
230 | # but keep track of directories ending in .cache
231 | !?*.[Cc]ache/
232 |
233 | # Others
234 | ClientBin/
235 | ~$*
236 | *~
237 | *.dbmdl
238 | *.dbproj.schemaview
239 | *.jfm
240 | *.pfx
241 | *.publishsettings
242 | orleans.codegen.cs
243 |
244 | # Including strong name files can present a security risk
245 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
246 | #*.snk
247 |
248 | # Since there are multiple workflows, uncomment next line to ignore bower_components
249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
250 | #bower_components/
251 |
252 | # RIA/Silverlight projects
253 | Generated_Code/
254 |
255 | # Backup & report files from converting an old project file
256 | # to a newer Visual Studio version. Backup files are not needed,
257 | # because we have git ;-)
258 | _UpgradeReport_Files/
259 | Backup*/
260 | UpgradeLog*.XML
261 | UpgradeLog*.htm
262 | ServiceFabricBackup/
263 | *.rptproj.bak
264 |
265 | # SQL Server files
266 | *.mdf
267 | *.ldf
268 | *.ndf
269 |
270 | # Business Intelligence projects
271 | *.rdl.data
272 | *.bim.layout
273 | *.bim_*.settings
274 | *.rptproj.rsuser
275 | *- [Bb]ackup.rdl
276 | *- [Bb]ackup ([0-9]).rdl
277 | *- [Bb]ackup ([0-9][0-9]).rdl
278 |
279 | # Microsoft Fakes
280 | FakesAssemblies/
281 |
282 | # GhostDoc plugin setting file
283 | *.GhostDoc.xml
284 |
285 | # Node.js Tools for Visual Studio
286 | .ntvs_analysis.dat
287 | node_modules/
288 |
289 | # Visual Studio 6 build log
290 | *.plg
291 |
292 | # Visual Studio 6 workspace options file
293 | *.opt
294 |
295 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
296 | *.vbw
297 |
298 | # Visual Studio LightSwitch build output
299 | **/*.HTMLClient/GeneratedArtifacts
300 | **/*.DesktopClient/GeneratedArtifacts
301 | **/*.DesktopClient/ModelManifest.xml
302 | **/*.Server/GeneratedArtifacts
303 | **/*.Server/ModelManifest.xml
304 | _Pvt_Extensions
305 |
306 | # Paket dependency manager
307 | .paket/paket.exe
308 | paket-files/
309 |
310 | # FAKE - F# Make
311 | .fake/
312 |
313 | # CodeRush personal settings
314 | .cr/personal
315 |
316 | # Python Tools for Visual Studio (PTVS)
317 | __pycache__/
318 | *.pyc
319 |
320 | # Cake - Uncomment if you are using it
321 | # tools/**
322 | # !tools/packages.config
323 |
324 | # Tabs Studio
325 | *.tss
326 |
327 | # Telerik's JustMock configuration file
328 | *.jmconfig
329 |
330 | # BizTalk build output
331 | *.btp.cs
332 | *.btm.cs
333 | *.odx.cs
334 | *.xsd.cs
335 |
336 | # OpenCover UI analysis results
337 | OpenCover/
338 |
339 | # Azure Stream Analytics local run output
340 | ASALocalRun/
341 |
342 | # MSBuild Binary and Structured Log
343 | *.binlog
344 |
345 | # NVidia Nsight GPU debugger configuration file
346 | *.nvuser
347 |
348 | # MFractors (Xamarin productivity tool) working folder
349 | .mfractor/
350 |
351 | # Local History for Visual Studio
352 | .localhistory/
353 |
354 | # BeatPulse healthcheck temp database
355 | healthchecksdb
356 |
357 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
358 | MigrationBackup/
359 |
360 | # Ionide (cross platform F# VS Code tools) working folder
361 | .ionide/
362 |
363 | # Fody - auto-generated XML schema
364 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | # 20230216 (2022/02/16)
2 | - 因为555dy网更新导致原先得解析不可用,特此更新以保证可以正常使用
3 |
4 | # 20221208 (2022/12/08)
5 | - 插件从主项目中分离,同时优化已有插件得bug
6 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Harlan-H
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 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Class1.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H._555dd7.plugin.Streams;
2 | using M3u8Downloader_H.Plugin;
3 |
4 | namespace M3u8Downloader_H._555dd7.plugin
5 | {
6 | public class Class1 : IPluginBuilder
7 | {
8 | public IM3u8FileInfoStreamService? CreateM3U8FileInfoStreamService() => null;
9 |
10 | public IM3uFileReader? CreateM3u8FileReader() => null;
11 |
12 | public IM3u8UriProvider? CreateM3u8UriProvider() => new StreamClient();
13 |
14 | public IDownloadService? CreatePluginService() => null;
15 |
16 | public void SetAttributeReader(IAttributeReaderManager attributeReader)
17 | {
18 |
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/M3u8Downloader_H.555dd7.plugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | M3u8Downloader_H._555dd7.plugin
6 | enable
7 | enable
8 | 1.0.0.2
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Models/PlayerInfo.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace M3u8Downloader_H._555dd7.plugin.Models
9 | {
10 | internal class PlayerInfo
11 | {
12 | [JsonProperty("url")]
13 | public string Url { get; set; } = default!;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Models/PlayerRequest.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H._555dd7.plugin.Utils;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace M3u8Downloader_H._555dd7.plugin.Models
9 | {
10 | internal readonly struct PlayerRequest
11 | {
12 | private readonly string playerUrl;
13 | private readonly string _userAgent;
14 | public PlayerRequest(string playerUrl, string userAgent)
15 | {
16 | this.playerUrl = playerUrl + "/get_play_url";
17 | _userAgent = userAgent;
18 | }
19 |
20 | public string AppKey => "www.555dy.com".GetMD5HexString().ToLower();
21 | public string ClientKey => _userAgent.GetMD5HexString().ToLower();
22 | public string RequestToken => "https://zyz.sdljwomen.com".GetMD5HexString().ToLower();
23 | public string AccessToken => playerUrl.Replace("https:","").GetMD5HexString().ToLower();
24 |
25 | public string PlayUrl => playerUrl;
26 |
27 | public static implicit operator Uri(PlayerRequest playerRequest) =>
28 | new($"{playerRequest.PlayUrl}?app_key={playerRequest.AppKey}&client_key={playerRequest.ClientKey}&request_token={playerRequest.RequestToken}&access_token={playerRequest.AccessToken}");
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Models/Response.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace M3u8Downloader_H._555dd7.plugin.Models
4 | {
5 | internal class Response
6 | {
7 | public int Code { get; set; }
8 |
9 | public Data? Data { get; set; }
10 |
11 | [JsonProperty("msg")]
12 | public string? Msg { get; set; }
13 | }
14 |
15 | internal class Data
16 | {
17 | public Uri Url { get; set; } = default!;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Streams/Extractor.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H._555dd7.plugin.Models;
2 | using M3u8Downloader_H._555dd7.plugin.Utils;
3 | using M3u8Downloader_H.Common.Extensions;
4 | using Newtonsoft.Json;
5 | using System;
6 | using System.Text;
7 | using System.Text.RegularExpressions;
8 | using System.Threading;
9 |
10 | namespace M3u8Downloader_H._555dd7.plugin.Streams
11 | {
12 | internal class Extractor
13 | {
14 | private readonly HttpClient httpClient;
15 | private readonly IEnumerable>? headers;
16 | private static readonly Regex regex = new("player_aaaa=(.*?)", RegexOptions.Compiled);
17 | private static readonly Regex serverUrlRegex = new("'(https://.*?)'", RegexOptions.Compiled);
18 | private static readonly byte[] _key = Encoding.UTF8.GetBytes("55cc5c42a943afdc");
19 | private static readonly byte[] _iv = Encoding.UTF8.GetBytes("d11324dcscfe16c0");
20 | private static readonly string _hmacRawKey = "55ca5c4d11424dcecfe16c08a943afdc";
21 |
22 | public Extractor(HttpClient httpClient, IEnumerable>? headers)
23 | {
24 | this.httpClient = httpClient;
25 | this.headers = headers;
26 | }
27 |
28 | public async Task GetM3u8IndexUrl(VideoId videoId,CancellationToken cancellationToken)
29 | {
30 | Uri uri = videoId;
31 | var url = await GetMainPageUrl(uri, cancellationToken);
32 | var serverUrl = await GetServerUrl(uri,cancellationToken);
33 | return await GetPlayUrlFromServerUrl(url,serverUrl,cancellationToken);
34 | }
35 |
36 | private async Task GetPlayUrlFromServerUrl(string mainpageUrl, string serverUrl, CancellationToken cancellationToken)
37 | {
38 | var tmpHeaders = GetPlayerUrlHeaders(mainpageUrl, serverUrl);
39 | tmpHeaders.TryGetValue("User-Agent", out string? useragent);
40 | Uri RequestUri = new PlayerRequest(serverUrl, useragent ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36");
41 | var encryptedResp = await httpClient.GetStringAsync(RequestUri, tmpHeaders, cancellationToken);
42 | var decryptText = encryptedResp.ToHexBytes().AesDecrypt(_key, _iv).GetString();
43 | var r = JsonConvert.DeserializeObject(decryptText);
44 | if (r is null)
45 | throw new InvalidDataException("返回内容序列化失败");
46 |
47 | if (r.Code != 200)
48 | throw new InvalidDataException($"数据返回异常,错误内容:{r.Msg}");
49 |
50 | return r.Data!.Url;
51 | }
52 |
53 | private Dictionary GetPlayerUrlHeaders(string url,string serverurl)
54 | {
55 | Dictionary tmpHeaders = headers is not null ? new Dictionary(headers) : new();
56 | long now = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds();;
57 | string SecondString = Convert.ToString(now);
58 | tmpHeaders.Add("X-PLAYER-TIMESTAMP", SecondString);
59 | tmpHeaders.Add("X-PLAYER-METHOD", "GET");
60 |
61 | var encryptedUrl = url.ToBytes().AesEncrypt(_key, _iv).GetHexString();
62 | tmpHeaders.Add("X-PLAYER-PACK", encryptedUrl);
63 |
64 | var hmacKeyBytes = (serverurl + "GET" + SecondString + _hmacRawKey).GetMD5HexString().ToLower().ToBytes();
65 | byte[] encryptData = encryptedUrl.ToBytes().HmacSha256(hmacKeyBytes);
66 | tmpHeaders.Add("X-PLAYER-SIGNATURE", Convert.ToHexString(encryptData).ToLower());
67 | return tmpHeaders;
68 | }
69 |
70 |
71 | private async Task GetMainPageUrl(Uri uri, CancellationToken cancellationToken)
72 | {
73 | var raw = await httpClient.GetStringAsync(uri, headers, cancellationToken);
74 | var initState = ExtractInitState(raw);
75 | PlayerInfo? playerInfo = JsonConvert.DeserializeObject(initState);
76 | if (playerInfo is null)
77 | throw new InvalidDataException("主页关键数据反序列化失败");
78 |
79 | return playerInfo.Url;
80 | }
81 |
82 | private async Task GetServerUrl(Uri baseUri, CancellationToken cancellationToken = default)
83 | {
84 | Uri uri = new(baseUri, "/player.html");
85 | var raw = await httpClient.GetStringAsync(uri, headers, cancellationToken);
86 | var urlGroups = serverUrlRegex.Matches(raw);
87 | if (urlGroups.Count < 2)
88 | throw new InvalidDataException("server url地址获取失败");
89 |
90 | var randomNum = new Random().Next(urlGroups.Count);
91 | return urlGroups[randomNum].Groups[1].Value;
92 | }
93 |
94 | private static string ExtractInitState(string raw)
95 | {
96 | var initState = regex.Match(raw).Groups[1].Value;
97 | if (string.IsNullOrWhiteSpace(initState))
98 | throw new InvalidDataException("没有获取到网页关键数据");
99 |
100 | return initState;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Streams/StreamClient.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Plugin;
2 |
3 | namespace M3u8Downloader_H._555dd7.plugin.Streams
4 | {
5 | internal class StreamClient : IM3u8UriProvider
6 | {
7 | public async Task GetM3u8UriAsync(HttpClient httpClient, Uri uri, IEnumerable>? headers, CancellationToken cancellationToken = default)
8 | {
9 | Extractor extractor = new(httpClient, headers);
10 | return await extractor.GetM3u8IndexUrl(uri, cancellationToken);
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Streams/VideoId.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Extensions;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace M3u8Downloader_H._555dd7.plugin.Streams
5 | {
6 | //@"vodplay/\d+-\d+-\d+\.html"
7 | public readonly partial struct VideoId
8 | {
9 | private string Value { get; }
10 |
11 | private VideoId(string value) => Value = value;
12 |
13 | public override string ToString() => Value;
14 | }
15 |
16 | public readonly partial struct VideoId
17 | {
18 | private static readonly Regex videoRegex = new(@"vodplay/\d+-\d+-\d+\.html", RegexOptions.Compiled);
19 | private static string? TryNormalize(string? url)
20 | {
21 | if (string.IsNullOrWhiteSpace(url))
22 | return null;
23 |
24 | if (videoRegex.IsMatch(url))
25 | return url;
26 |
27 | return null;
28 | }
29 |
30 | public static VideoId? TryParse(string? url) => TryNormalize(url)?.Pipe(r => new VideoId(r));
31 |
32 | public static VideoId Parse(string url) => TryParse(url) ?? throw new InvalidDataException("不能解析得地址,必须类似 https://www.xxxxx.com/vodplay/390618-1-8.html");
33 |
34 | public static implicit operator VideoId(Uri url) => Parse(url.OriginalString);
35 |
36 | public static implicit operator Uri(VideoId videoid) => new(videoid.ToString());
37 | }
38 |
39 | public partial struct VideoId : IEquatable
40 | {
41 | public bool Equals(VideoId other) => StringComparer.Ordinal.Equals(Value, other.Value);
42 | public override bool Equals(object? obj) => obj is VideoId other && Equals(other);
43 | public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
44 |
45 | public static bool operator ==(VideoId left, VideoId right) => left.Equals(right);
46 | public static bool operator !=(VideoId left, VideoId right) => !(left == right);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Utils/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Cryptography;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace M3u8Downloader_H._555dd7.plugin.Utils
9 | {
10 | internal static class StringExtensions
11 | {
12 | public static string GetMD5HexString(this string s)
13 | {
14 | byte[] data = s.GetMd5Bytes();
15 | return Convert.ToHexString(data);
16 | }
17 |
18 | public static byte[] GetMd5Bytes(this string s )
19 | {
20 | using MD5 md5 = MD5.Create();
21 | return md5.ComputeHash(Encoding.UTF8.GetBytes(s));
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.555dd7.plugin/Utils/WebSocketClient.cs:
--------------------------------------------------------------------------------
1 | using System.Net.WebSockets;
2 |
3 | namespace M3u8Downloader_H._555dd7.plugin.Utils
4 | {
5 | internal class WebSocketClient : IDisposable
6 | {
7 | private readonly ClientWebSocket _clientWebSocket;
8 | public WebSocketClient()
9 | {
10 | _clientWebSocket = new ClientWebSocket();
11 | }
12 |
13 | public async Task SendAsync(Uri uri, ReadOnlyMemory buffer ,CancellationToken cancellationToken)
14 | {
15 | await _clientWebSocket.ConnectAsync(uri, cancellationToken);
16 | await _clientWebSocket.SendAsync(buffer, WebSocketMessageType.Binary,true, cancellationToken);
17 | var receiveBuffer = new byte[1024];
18 | Memory memory = new(receiveBuffer);
19 | int count = 0;
20 | while (true)
21 | {
22 | var resp = await _clientWebSocket.ReceiveAsync(memory.Slice(count, receiveBuffer.Length), cancellationToken);
23 | count += resp.Count;
24 | if (resp.EndOfMessage)
25 | break;
26 | }
27 | await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken);
28 | return memory[..count].ToArray();
29 | }
30 |
31 | public void Dispose()
32 | {
33 | _clientWebSocket.Dispose();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Extensions/ConvertExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace M3u8Downloader_H.Common.Extensions
8 | {
9 | public static class ConvertExtensions
10 | {
11 | public static byte[] ToBytes(this string s) => Encoding.UTF8.GetBytes(s);
12 |
13 | public static string GetString(this byte[] bytes) => Encoding.UTF8.GetString(bytes);
14 |
15 | public static byte[] ToHexBytes(this string s) => Convert.FromHexString(s);
16 |
17 | public static string GetHexString(this byte[] bytes) => Convert.ToHexString(bytes);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Extensions/CryptExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Security.Cryptography;
3 | using System.Text;
4 |
5 | namespace M3u8Downloader_H.Common.Extensions
6 | {
7 | public static class CryptExtensions
8 | {
9 | private static readonly Dictionary KeyGroup = new() { { "AES-128", (16, 24) }, { "AES-192", (24, 32) }, { "AES-256", (32, 44) } };
10 |
11 | public static byte[] HmacSha256(this Stream memory, byte[] key)
12 | {
13 | using HMACSHA256 hmac = new(key);
14 | return hmac.ComputeHash(memory);
15 | }
16 |
17 | public static byte[] HmacSha256(this byte[] memory, byte[] key)
18 | {
19 | using HMACSHA256 hmac = new(key);
20 | return hmac.ComputeHash(memory);
21 | }
22 |
23 | public static byte[] AesEncrypt(this byte[] content, byte[] aesKey, byte[] aesIv)
24 | {
25 | using Aes _aes = Aes.Create();
26 | _aes.Padding = PaddingMode.PKCS7;
27 | _aes.Mode = CipherMode.CBC;
28 | _aes.Key = aesKey;
29 | _aes.IV = aesIv ?? new byte[16];
30 |
31 | using var _crypto = _aes.CreateEncryptor(_aes.Key, _aes.IV);
32 | return _crypto.TransformFinalBlock(content, 0, content.Length);
33 | }
34 |
35 | public static Stream AesEncrypt(this Stream memory, byte[] aesKey, byte[] aesIv)
36 | {
37 | using Aes _aes = Aes.Create();
38 | _aes.Padding = PaddingMode.PKCS7;
39 | _aes.Mode = CipherMode.CBC;
40 | _aes.Key = aesKey;
41 | _aes.IV = aesIv ?? new byte[16];
42 |
43 | var _crypto = _aes.CreateEncryptor(_aes.Key, _aes.IV);
44 | return new CryptoStream(memory, _crypto, CryptoStreamMode.Write);
45 | }
46 |
47 | public static byte[] AesDecrypt(this byte[] content, byte[] aesKey, byte[] aesIV)
48 | {
49 |
50 | using Aes _aes = Aes.Create();
51 | _aes.Padding = PaddingMode.PKCS7;
52 | _aes.Mode = CipherMode.CBC;
53 | _aes.Key = aesKey;
54 | _aes.IV = aesIV ?? new byte[16];
55 |
56 | using var _crypto = _aes.CreateDecryptor(_aes.Key, _aes.IV);
57 | return _crypto.TransformFinalBlock(content, 0, content.Length);
58 | }
59 |
60 | public static Stream AesDecrypt(this Stream memory, byte[] aesKey, byte[] aesIV)
61 | {
62 | using Aes aesAlg = Aes.Create();
63 | aesAlg.Padding = PaddingMode.PKCS7;
64 | aesAlg.Mode = CipherMode.CBC;
65 | aesAlg.Key = aesKey;
66 | aesAlg.IV = aesIV ?? new byte[16];
67 |
68 | ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
69 | return new CryptoStream(memory, decryptor, CryptoStreamMode.Read);
70 | }
71 |
72 | public static byte[] TryParseKey(this byte[] data, string method)
73 | {
74 | string tmpMethod = string.IsNullOrWhiteSpace(method) ? "AES-128" : method.ToUpper(CultureInfo.CurrentCulture).Trim();
75 | if (KeyGroup.TryGetValue(tmpMethod, out (int, int) tmpKey))
76 | {
77 | if (data.Length == tmpKey.Item1)
78 | return data;
79 | else if (data.Length == tmpKey.Item2)
80 | {
81 | var stringdata = Encoding.UTF8.GetString(data);
82 | return Convert.FromBase64String(stringdata);
83 | }
84 | }
85 | throw new InvalidCastException($"无法解析的密钥,请确定是否为AES-128,AES-192,AES-256");
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Extensions/GenericExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace M3u8Downloader_H.Common.Extensions
8 | {
9 | public static class GenericExtensions
10 | {
11 | public static TOut Pipe(this TIn input, Func transform) => transform(input);
12 |
13 | public static T Clamp(this T value, T min, T max) where T : IComparable =>
14 | value.CompareTo(min) <= 0
15 | ? min
16 | : value.CompareTo(max) >= 0
17 | ? max
18 | : value;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Extensions/HttpClientExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net.Http;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace M3u8Downloader_H.Common.Extensions
9 | {
10 | public static class HttpClientExtension
11 | {
12 | public static async Task GetStreamAsync(this HttpClient httpClient, HttpRequestMessage httpRequestMessage, bool ensureSuccess = true, CancellationToken cancellationToken = default)
13 | {
14 | HttpResponseMessage response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
15 | if(ensureSuccess) response.EnsureSuccessStatusCode();
16 |
17 | return await response.Content.ReadAsStreamAsync(cancellationToken);
18 | }
19 |
20 | public static async Task GetStreamAsync(this HttpClient httpClient,Uri uri, IEnumerable>? headers = default,CancellationToken cancellationToken = default)
21 | {
22 | using HttpRequestMessage request = new(HttpMethod.Get, uri);
23 | request.AddHeaders(headers);
24 |
25 | return await httpClient.GetStreamAsync(request, true, cancellationToken);
26 | }
27 |
28 | public static async Task<(Uri?,Stream)> GetStreamAndUriAsync(this HttpClient httpClient, Uri uri, IEnumerable>? headers = default, CancellationToken cancellationToken = default)
29 | {
30 | using HttpRequestMessage request = new(HttpMethod.Get, uri);
31 | request.AddHeaders(headers);
32 |
33 | HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
34 | response.EnsureSuccessStatusCode();
35 |
36 | Uri? requestUri = response.RequestMessage?.RequestUri;
37 | Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
38 | return (requestUri, stream);
39 | }
40 |
41 | public static async Task GetStreamAsync(this HttpClient httpClient,string uri, IEnumerable>? headers = default, CancellationToken cancellationToken = default)
42 | {
43 | return await httpClient.GetStreamAsync(new Uri(uri, UriKind.Absolute), headers, cancellationToken);
44 | }
45 |
46 | public static async Task GetByteArrayAsync(this HttpClient httpClient, HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken = default)
47 | {
48 | HttpResponseMessage response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
49 | response.EnsureSuccessStatusCode();
50 |
51 | return await response.Content.ReadAsByteArrayAsync(cancellationToken);
52 | }
53 |
54 | public static async Task GetByteArrayAsync(this HttpClient httpClient, Uri uri, IEnumerable>? headers = default, CancellationToken cancellationToken = default)
55 | {
56 | using HttpRequestMessage request = new(HttpMethod.Get, uri);
57 | request.AddHeaders(headers);
58 |
59 | return await httpClient.GetByteArrayAsync(request, cancellationToken);
60 | }
61 |
62 | public static async Task GetByteArrayAsync(this HttpClient httpClient, string uri, IEnumerable>? headers = default, CancellationToken cancellationToken = default)
63 | {
64 | return await httpClient.GetByteArrayAsync(new Uri(uri, UriKind.Absolute), headers, cancellationToken);
65 | }
66 |
67 | public static async Task GetStringAsync(this HttpClient httpClient, HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken = default)
68 | {
69 | HttpResponseMessage response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
70 | response.EnsureSuccessStatusCode();
71 |
72 | return await response.Content.ReadAsStringAsync(cancellationToken);
73 | }
74 |
75 |
76 | public static async Task GetStringAsync(this HttpClient httpClient,Uri uri, IEnumerable>? headers = default, CancellationToken cancellationToken = default)
77 | {
78 | using HttpRequestMessage request = new(HttpMethod.Get, uri);
79 | request.AddHeaders(headers);
80 |
81 | return await httpClient.GetStringAsync(request, cancellationToken);
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Extensions/HttpRequestMessageExtension.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Net.Http;
3 |
4 | namespace M3u8Downloader_H.Common.Extensions
5 | {
6 | public static class HttpRequestMessageExtension
7 | {
8 | public static void AddHeaders(this HttpRequestMessage httpRequestMessage, IEnumerable>? headers)
9 | {
10 | if (headers == null)
11 | return;
12 |
13 | foreach (var item in headers)
14 | {
15 | if (!string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value))
16 | httpRequestMessage.Headers.TryAddWithoutValidation(item.Key, item.Value);
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography;
3 | using System.Text;
4 |
5 | namespace M3u8Downloader_H.Common.Extensions
6 | {
7 | public static class StringExtensions
8 | {
9 |
10 | public static string? NullIfWhiteSpace(this string s) =>
11 | !string.IsNullOrWhiteSpace(s)
12 | ? s
13 | : null;
14 |
15 |
16 | public static string GetMd5(this string s)
17 | {
18 | using SHA256 md5 = SHA256.Create();
19 | byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(s));
20 | return Convert.ToHexString(data);
21 | }
22 |
23 | public static byte[] ToHex(this string s)
24 | {
25 | s = s.StartsWith("0x", StringComparison.Ordinal) ? s[2..] : s;
26 | return Convert.FromHexString(s);
27 | }
28 |
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/M3u8Downloader_H.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/M3u8Infos/M3UFileInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace M3u8Downloader_H.Common.M3u8Infos
6 | {
7 | public partial class M3UFileInfo
8 | {
9 | public int? Version { get; set; }
10 |
11 | public int? MediaSequence { get; set; }
12 |
13 | public int? TargetDuration { get; set; }
14 |
15 | public bool? AllowCache { get; set; }
16 |
17 | public string PlaylistType { get; set; } = default!;
18 |
19 | public M3UMediaInfo? Map { get; set; }
20 |
21 | public DateTime? ProgramDateTime { get; set; }
22 |
23 | public M3UKeyInfo Key { get; set; } = default!;
24 |
25 | public IList Streams { get; set; } = default!;
26 |
27 | public IList MediaFiles { get; set; } = default!;
28 |
29 | //当原始的m3u8中的数据 不满足需求的时候 可以通过自定义的数据 进行操作
30 | public object? UserData { get; set; }
31 |
32 | public M3UFileInfo(M3UFileInfo m3UFileInfo)
33 | {
34 | Version = m3UFileInfo.Version;
35 | MediaSequence = m3UFileInfo.MediaSequence;
36 | TargetDuration = m3UFileInfo.TargetDuration;
37 | AllowCache = m3UFileInfo.AllowCache;
38 | PlaylistType = m3UFileInfo.PlaylistType;
39 | Key = m3UFileInfo.Key;
40 | Streams = m3UFileInfo.Streams.ToList();
41 | MediaFiles = m3UFileInfo.MediaFiles.ToList();
42 | }
43 |
44 |
45 |
46 | public M3UFileInfo()
47 | {
48 |
49 | }
50 | }
51 |
52 | public partial class M3UFileInfo
53 | {
54 | public static M3UFileInfo CreateVodM3UFileInfo()
55 | {
56 | M3UFileInfo m3UFileInfo = new()
57 | {
58 | PlaylistType = "VOD"
59 | };
60 | return m3UFileInfo;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/M3u8Infos/M3UKeyInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace M3u8Downloader_H.Common.M3u8Infos
4 | {
5 | public partial class M3UKeyInfo
6 | {
7 | public string Method { get; set; } = default!;
8 |
9 | public Uri Uri { get; set; } = default!;
10 |
11 | public byte[] BKey { get; set; } = default!;
12 |
13 | public byte[] IV { get; set; } = default!;
14 | }
15 |
16 | public partial class M3UKeyInfo :IEquatable
17 | {
18 | public bool Equals(M3UKeyInfo? other) => Object.ReferenceEquals(this, other);
19 |
20 | public override bool Equals(object? obj) => obj is M3UKeyInfo && Equals(obj);
21 |
22 | public override int GetHashCode() => StringComparer.Ordinal.GetHashCode();
23 |
24 | public static bool operator ==(M3UKeyInfo M3UKeyInfo1, M3UKeyInfo M3UKeyInfo2) => M3UKeyInfo1.Equals(M3UKeyInfo2);
25 | public static bool operator !=(M3UKeyInfo M3UKeyInfo1, M3UKeyInfo M3UKeyInfo2) => !(M3UKeyInfo1 == M3UKeyInfo2);
26 | }
27 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/M3u8Infos/M3UMediaInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http.Headers;
3 |
4 | namespace M3u8Downloader_H.Common.M3u8Infos
5 | {
6 | public partial class M3UMediaInfo
7 | {
8 | public float Duration { get; set; }
9 |
10 | public string Title { get; set; } = default!;
11 |
12 | public Uri Uri { get; set; } = default!;
13 |
14 | public RangeHeaderValue? RangeValue { get; set; }
15 |
16 | }
17 |
18 | public partial class M3UMediaInfo : IEquatable
19 | {
20 | public bool Equals(M3UMediaInfo? other) => StringComparer.Ordinal.Equals(Uri, other?.Uri);
21 | public override bool Equals(object? obj) => obj is M3UMediaInfo other && Equals(other);
22 | public override int GetHashCode() => Uri.GetHashCode();
23 |
24 | public static bool operator ==(M3UMediaInfo m3UMediaInfo, M3UMediaInfo m3UMediaInfo1) => m3UMediaInfo.Equals(m3UMediaInfo1);
25 | public static bool operator !=(M3UMediaInfo m3UMediaInfo, M3UMediaInfo m3UMediaInfo1) => !(m3UMediaInfo == m3UMediaInfo1);
26 |
27 | }
28 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/M3u8Infos/M3UStreamInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace M3u8Downloader_H.Common.M3u8Infos
4 | {
5 | public class M3UStreamInfo
6 | {
7 | public int? ProgramId { get; set; }
8 |
9 | public int? Bandwidth { get; set; }
10 |
11 | public string Codecs { get; set; } = default!;
12 |
13 | public string Resolution { get; set; } = default!;
14 |
15 | public Uri Uri { get; set; } = default!;
16 | }
17 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Utils/JsonPathConverter.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 | using Newtonsoft.Json;
3 |
4 | namespace M3u8Downloader_H.Common.Utils
5 | {
6 | public class JsonPathConverter : JsonConverter
7 | {
8 | public override bool CanWrite => false;
9 |
10 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
11 | {
12 | var jObject = JObject.Load(reader);
13 | var targetObj = Activator.CreateInstance(objectType);
14 |
15 | foreach (var prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
16 | {
17 | var jsonPropertyAttr = prop.GetCustomAttributes(true).OfType().FirstOrDefault();
18 | if (jsonPropertyAttr == null)
19 | {
20 | throw new JsonReaderException($"{nameof(JsonPropertyAttribute)} is mandatory when using {nameof(JsonPathConverter)}");
21 | }
22 |
23 | var jsonPath = jsonPropertyAttr.PropertyName;
24 | var token = jObject.SelectToken(jsonPath!);
25 |
26 | if (token != null && token.Type != JTokenType.Null)
27 | {
28 | var jsonConverterAttr = prop.GetCustomAttributes(true).OfType().FirstOrDefault();
29 | object? value;
30 | if (jsonConverterAttr is null)
31 | {
32 | serializer.Converters.Clear();
33 | value = token.ToObject(prop.PropertyType, serializer);
34 | }
35 | else
36 | {
37 | value = JsonConvert.DeserializeObject(token.ToString(), prop.PropertyType,
38 | (JsonConverter?)Activator.CreateInstance(jsonConverterAttr.ConverterType)!);
39 | }
40 | prop.SetValue(targetObj, value, null);
41 | }
42 | }
43 |
44 | return targetObj;
45 | }
46 |
47 | public override bool CanConvert(Type objectType)
48 | {
49 | return true;
50 | }
51 |
52 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
53 | {
54 | throw new NotImplementedException();
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Utils/KV.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace M3u8Downloader_H.Common.Utils
5 | {
6 | public static class KV
7 | {
8 | public static KeyValuePair Parse(string text, char separator = ':')
9 | {
10 | if (string.IsNullOrEmpty(text))
11 | return new KeyValuePair();
12 |
13 | var strArray = text.Split(
14 | new[] { separator }, 2, StringSplitOptions.RemoveEmptyEntries
15 | );
16 |
17 | if (strArray.Length == 2)
18 | {
19 | return new KeyValuePair(strArray[0].Trim(),
20 | (strArray.Length > 1 ? strArray[1] : string.Empty).Trim(' ', '"'));
21 | }
22 |
23 | return new KeyValuePair();
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Common/Utils/To.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace M3u8Downloader_H.Common.Utils
4 | {
5 | public static class To
6 | {
7 | private static bool? Bool(string text)
8 | {
9 | if (string.IsNullOrWhiteSpace(text))
10 | return new bool?();
11 | if (string.Equals(text, "YES"))
12 | return true;
13 |
14 | return string.Equals(text, "NO") ? false : new bool?(Convert.ToBoolean(text));
15 | }
16 |
17 | public static T? Value(string text) where T : struct
18 | {
19 | if (string.IsNullOrWhiteSpace(text))
20 | return new T?();
21 |
22 |
23 | var conversionType = typeof(T);
24 | if (conversionType == typeof(bool))
25 | {
26 | ValueType? valueType = Bool(text);
27 | return (T?)valueType;
28 | }
29 |
30 | try
31 | {
32 | return (T)Convert.ChangeType(text, conversionType);
33 | }
34 | catch
35 | {
36 | return new T?();
37 | }
38 | }
39 |
40 |
41 |
42 | }
43 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IAttributeReader.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.M3u8Infos;
2 |
3 | namespace M3u8Downloader_H.Plugin
4 | {
5 | public interface IAttributeReader
6 | {
7 | ///
8 | /// 是否提前退出,一般情况下都设置为false
9 | ///
10 | bool ShouldTerminate { get; }
11 |
12 | ///
13 | /// 匹配到的标签内容的主处理函数
14 | ///
15 | /// m3UFileInfo数据结构
16 | /// 匹配到标签的冒号之后的内容
17 | /// 控制当前取到的内容,或者取下一行内容的参数
18 | /// 请求的原始的主url,就是那个.m3u8的地址
19 | void Write(M3UFileInfo m3UFileInfo, string value, IEnumerator reader, Uri baseUri);
20 | }
21 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IAttributeReaderManager.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | namespace M3u8Downloader_H.Plugin
4 | {
5 | public interface IAttributeReaderManager
6 | {
7 | IAttributeReader this[string key] { get; set; }
8 |
9 | ///
10 | /// 当原始的标签没有的情况下可以使用此函数,如果想替换程序内部的某个标签,用上面的dict["key"]方式或者下方的set函数
11 | ///
12 | /// m3u8的标签,完整的包含#号
13 | /// 实现了IAttributeReader接口的类new进来
14 | void Add(string key, IAttributeReader value);
15 |
16 | ///
17 | /// 当需要添加新的标签处理或者覆盖原始的标签处理,使用此函数
18 | ///
19 | /// m3u8的标签,完整的包含#号
20 | /// 实现了IAttributeReader接口的类new进来
21 | void Set(string key, IAttributeReader value);
22 |
23 | ///
24 | /// 是否包含某个标签
25 | ///
26 | /// m3u8的标签,完整的包含#号
27 | /// 包含为真,否则为假
28 | bool ContainsKey(string key);
29 |
30 | ///
31 | /// 获取某个标签的值
32 | ///
33 | /// m3u8的标签,完整的包含#号
34 | /// 返回得到的值
35 | /// 包含此标签的为真,否则为假
36 | bool TryGetValue(string key, [MaybeNullWhen(false)] out IAttributeReader value);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IDownloadService.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.M3u8Infos;
2 |
3 | namespace M3u8Downloader_H.Plugin
4 | {
5 | public interface IDownloadService
6 | {
7 | ///
8 | /// 初始化函数,在没有进行任何下载之前,第一个调用的函数
9 | ///
10 | /// http实例
11 | /// 附带的请求头,如果有的话
12 | /// m3u8的数据
13 | /// 取消的token
14 | /// 没有返回内容
15 | Task Initialize(HttpClient httpClient, IEnumerable>? headers, M3UFileInfo m3UFileInfo, CancellationToken cancellationToken);
16 |
17 |
18 | ///
19 | /// 当下载的数据接收到之后,会请求此函数,以便做后续处理
20 | /// 例如 当某些数据他没有采用通用的加密方式,而是使用了自己指定的一些加密方式,即可实现此函数来进一步处理
21 | /// 如果不需要处理此函数 可以直接将参数stream直接返回即可
22 | ///
23 | /// 请求到的数据流
24 | /// 取消的token
25 | /// 返回处理后的数据
26 | Stream HandleData(Stream stream, CancellationToken cancellationToken);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IM3u8FileInfoStreamService.cs:
--------------------------------------------------------------------------------
1 | namespace M3u8Downloader_H.Plugin
2 | {
3 | public interface IM3u8FileInfoStreamService
4 | {
5 | ///
6 | /// 获取m3u8得数据流
7 | /// 当你实现此函数得时候需要注意得是
8 | /// 这个请求数据他可能会循环请求,因为某些m3u8得数据流是需要请求两次得
9 | /// 所以当你实现此函数得时候,需要注意一下是否被重复调用了
10 | /// 如果需要默认得处理函数可以使用下面得方式,同时引入M3u8Downloader_H.Common这个dll
11 | /// httpClient.GetStreamAndUriAsync(uri, headers, cancellationToken);
12 | ///
13 | /// http实例
14 | /// 请求地址
15 | /// 请求头
16 | /// 取消的令牌
17 | /// 返回得到得m3u8数据流
18 | Task<(Uri?,Stream)> GetM3u8FileStreamAsync(HttpClient httpClient, Uri uri, IEnumerable>? headers, CancellationToken cancellationToken = default);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IM3u8UriProvider.cs:
--------------------------------------------------------------------------------
1 | namespace M3u8Downloader_H.Plugin
2 | {
3 | public interface IM3u8UriProvider
4 | {
5 | ///
6 | /// 获取m3u8得数据流,此函数可以做解析网站得操作,只要最终返回m3u8得数据流即可
7 | ///
8 | /// http实例
9 | /// 请求地址
10 | /// 请求头
11 | /// 取消的令牌
12 | /// 返回m3u8得地址
13 | Task GetM3u8UriAsync(HttpClient httpClient, Uri uri, IEnumerable>? headers, CancellationToken cancellationToken = default);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IM3uFileReader.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.M3u8Infos;
2 |
3 | namespace M3u8Downloader_H.Plugin
4 | {
5 | public interface IM3uFileReader
6 | {
7 | M3UFileInfo Read(Uri baseUri, Stream stream);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/IPluginBuilder.cs:
--------------------------------------------------------------------------------
1 | namespace M3u8Downloader_H.Plugin
2 | {
3 | public interface IPluginBuilder
4 | {
5 | ///
6 | /// 创建获取m3u8 uri的类
7 | /// 如果你不需要处理,只要return null 即可
8 | ///
9 | /// 返回实现了IGetM3u8InfoService接口的实例化
10 | IM3u8UriProvider? CreateM3u8UriProvider();
11 |
12 | ///
13 | /// 获取m3u8文件流得类
14 | /// 如果你需要对某项请求或者响应加密或者解密,可以实现此类
15 | ///
16 | ///
17 | IM3u8FileInfoStreamService? CreateM3U8FileInfoStreamService();
18 |
19 |
20 | ///
21 | /// 解析m3u8数据得接口,在大部分情况之下此接口是不用实现得
22 | /// 在某些情况下你不需要默认得解析方案,那么请实现此接口
23 | /// 如果你不需要处理,直接return null
24 | ///
25 | /// 返回实现了IM3uFileReader接口得实例
26 | IM3uFileReader? CreateM3u8FileReader();
27 |
28 | ///
29 | /// 对原始的m3u8中的一些标签进行修改,或者增加自己需要对某些标签的特别处理
30 | ///
31 | /// 实现IAttributeReaderManager的实例
32 | void SetAttributeReader(IAttributeReaderManager attributeReader);
33 |
34 | ///
35 | /// 如果需要对某些数据进行特殊的处理,例如需要某些流数据使用的特殊加密,那需要实现返回IDownloadService的实例
36 | /// 例如
37 | /// class MyDownload : IDownloadService {}
38 | ///
39 | /// IDownloadService? CreatePluginService()
40 | /// {
41 | /// return new MyDownload();
42 | /// }
43 | ///
44 | /// 如果你不需要处理得到的流,只要return null 即可
45 | ///
46 | /// 返回实现了IDownloadService接口的实例化
47 | IDownloadService? CreatePluginService();
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugin.Abstractions/M3u8Downloader_H.Plugin.Abstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.Plugins.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32929.385
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "M3u8Downloader_H.vlive.plugin", "M3u8Downloader_H.vlive.plugin\M3u8Downloader_H.vlive.plugin.csproj", "{8FF29452-0F0F-49A4-91EA-C75282150299}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "M3u8Downloader_H.Common", "M3u8Downloader_H.Common\M3u8Downloader_H.Common.csproj", "{714AA995-B712-476E-B809-DE02435D1C97}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "M3u8Downloader_H.Plugin.Abstractions", "M3u8Downloader_H.Plugin.Abstractions\M3u8Downloader_H.Plugin.Abstractions.csproj", "{20F6DD0B-0870-42BE-8D75-7DAB36174871}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "M3u8Downloader_H.555dd7.plugin", "M3u8Downloader_H.555dd7.plugin\M3u8Downloader_H.555dd7.plugin.csproj", "{1ED26CF2-4F86-4E75-835D-26CC025549DD}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "M3u8Downloader_H.bilibili.plugin", "M3u8Downloader_H.bilibili.plugin\M3u8Downloader_H.bilibili.plugin.csproj", "{4C727F71-290D-4BAE-83B5-0E837A46AA04}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "M3u8Downloader_H.douyin.plugin", "M3u8Downloader_H.douyin.plugin\M3u8Downloader_H.douyin.plugin.csproj", "{7C9D015B-7980-4252-AA8E-75AA8A536EAA}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "M3u8Downloader_H.Test", "M3u8Downloader_H.Test\M3u8Downloader_H.Test.csproj", "{6A5DBDDE-07CE-4E95-8361-78BB3CE38DD9}"
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Release|Any CPU = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {8FF29452-0F0F-49A4-91EA-C75282150299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {8FF29452-0F0F-49A4-91EA-C75282150299}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {8FF29452-0F0F-49A4-91EA-C75282150299}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {8FF29452-0F0F-49A4-91EA-C75282150299}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {714AA995-B712-476E-B809-DE02435D1C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {714AA995-B712-476E-B809-DE02435D1C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {714AA995-B712-476E-B809-DE02435D1C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {714AA995-B712-476E-B809-DE02435D1C97}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {20F6DD0B-0870-42BE-8D75-7DAB36174871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {20F6DD0B-0870-42BE-8D75-7DAB36174871}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {20F6DD0B-0870-42BE-8D75-7DAB36174871}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {20F6DD0B-0870-42BE-8D75-7DAB36174871}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {1ED26CF2-4F86-4E75-835D-26CC025549DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {1ED26CF2-4F86-4E75-835D-26CC025549DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {1ED26CF2-4F86-4E75-835D-26CC025549DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {1ED26CF2-4F86-4E75-835D-26CC025549DD}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {4C727F71-290D-4BAE-83B5-0E837A46AA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {4C727F71-290D-4BAE-83B5-0E837A46AA04}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {4C727F71-290D-4BAE-83B5-0E837A46AA04}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {4C727F71-290D-4BAE-83B5-0E837A46AA04}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {7C9D015B-7980-4252-AA8E-75AA8A536EAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {7C9D015B-7980-4252-AA8E-75AA8A536EAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {7C9D015B-7980-4252-AA8E-75AA8A536EAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {7C9D015B-7980-4252-AA8E-75AA8A536EAA}.Release|Any CPU.Build.0 = Release|Any CPU
50 | {6A5DBDDE-07CE-4E95-8361-78BB3CE38DD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {6A5DBDDE-07CE-4E95-8361-78BB3CE38DD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {6A5DBDDE-07CE-4E95-8361-78BB3CE38DD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {6A5DBDDE-07CE-4E95-8361-78BB3CE38DD9}.Release|Any CPU.Build.0 = Release|Any CPU
54 | EndGlobalSection
55 | GlobalSection(SolutionProperties) = preSolution
56 | HideSolutionNode = FALSE
57 | EndGlobalSection
58 | GlobalSection(ExtensibilityGlobals) = postSolution
59 | SolutionGuid = {9B559C85-3E64-4BDE-A77B-CA0FDFFF76E3}
60 | EndGlobalSection
61 | EndGlobal
62 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/Class1.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.bilibili.plugin.Lives;
2 | using M3u8Downloader_H.bilibili.plugin.Readers;
3 | using M3u8Downloader_H.Plugin;
4 |
5 | namespace M3u8Downloader_H.bilibili.plugin
6 | {
7 | public class Class1 : IPluginBuilder
8 | {
9 | public IM3u8UriProvider? CreateM3u8UriProvider() => new LiveClient();
10 |
11 | public IM3u8FileInfoStreamService? CreateM3U8FileInfoStreamService() => null;
12 |
13 | public IM3uFileReader? CreateM3u8FileReader() => null;
14 |
15 | public IDownloadService? CreatePluginService() => null;
16 |
17 | public void SetAttributeReader(IAttributeReaderManager attributeReader)
18 | {
19 | attributeReader.Set("#EXTINF", new MediaAttributeReader());
20 | }
21 |
22 |
23 | }
24 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/Extensions/StringExtension.cs:
--------------------------------------------------------------------------------
1 | namespace M3u8Downloader_H.bilibili.plugin.Extensions
2 | {
3 | internal static class StringExtension
4 | {
5 | public static string GetPath(this string s)
6 | {
7 | int pos = s.IndexOf('?');
8 | return pos == -1 ? s : s.Remove(pos);
9 | }
10 |
11 | public static string? GetQuery(this string s)
12 | {
13 | int pos = s.IndexOf('?');
14 | return pos == -1 ? null : s[(pos + 1)..];
15 | }
16 |
17 | public static Dictionary? GetQueryKeyPairs(this string s)
18 | {
19 | if (string.IsNullOrWhiteSpace(s))
20 | return null;
21 |
22 | var splitArr = s.Split("=");
23 | Dictionary queryDict = new();
24 | for (int i = 0; i < splitArr.Length; i+=2)
25 | {
26 | queryDict.Add(splitArr[i], splitArr[i + 1]);
27 | }
28 | return queryDict;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/Lives/LiveClient.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.bilibili.plugin.Models;
2 | using M3u8Downloader_H.Common.Extensions;
3 | using M3u8Downloader_H.Plugin;
4 | using Newtonsoft.Json;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace M3u8Downloader_H.bilibili.plugin.Lives
8 | {
9 | internal class LiveClient : IM3u8UriProvider
10 | {
11 | private static readonly Regex regex = new(@"__NEPTUNE_IS_MY_WAIFU__=(\{.*?\})", RegexOptions.Compiled);
12 | public async Task GetM3u8UriAsync(HttpClient httpClient, Uri uri, IEnumerable>? headers, CancellationToken cancellationToken = default)
13 | {
14 | if (uri.LocalPath.Contains("m3u8"))
15 | return uri;
16 |
17 | var raw = await GetLiveStreamInfo(httpClient,uri, headers, cancellationToken);
18 | var data= ExtractStreamInfos(raw);
19 | var requestUrl = GetRequestUrl(data);
20 | return new Uri(requestUrl, UriKind.Absolute);
21 | }
22 |
23 | private static async Task GetLiveStreamInfo(HttpClient httpClient,LiveId liveId, IEnumerable>? headers, CancellationToken cancellationToken = default)
24 | {
25 | using HttpRequestMessage request = new(HttpMethod.Get, liveId);
26 | request.AddHeaders(headers);
27 | if(!request.Headers.Contains("accept"))
28 | {
29 | request.Headers.Add("accept", "*/*");
30 | }
31 | return await httpClient.GetStringAsync(request, cancellationToken);
32 | }
33 |
34 | private static string ExtractStreamInfos(string raw)
35 | {
36 | var initState = regex.Match(raw).Groups[1].Value;
37 | if (string.IsNullOrWhiteSpace(initState))
38 | throw new InvalidDataException("没有获取到直播流得数据");
39 |
40 | return initState;
41 | }
42 |
43 | private static string GetRequestUrl(string raw)
44 | {
45 | var result = JsonConvert.DeserializeObject(raw);
46 | if (result is null)
47 | throw new InvalidDataException("解析直播数据失败");
48 |
49 | if (result.StreamInfos is null)
50 | throw new InvalidDataException("主播可能没有开播");
51 |
52 | var codecInfos = result.StreamInfos.Where(s => s.ProtocolName == "http_hls").Select(s => s.CodecInfos).FirstOrDefault();
53 | if(codecInfos is null)
54 | throw new InvalidDataException("获取直播m3u8地址失败");
55 |
56 | var requesturl = codecInfos
57 | .Where(e => e.FormatName == "fmp4")
58 | .Select(e => e.HostUrl + e.BaseUrl + e.Extra)
59 | .FirstOrDefault() ??
60 | codecInfos
61 | .Where(e => e.FormatName == "ts")
62 | .Select(e => e.HostUrl + e.BaseUrl + e.Extra)
63 | .FirstOrDefault();
64 |
65 | if (requesturl is null)
66 | throw new InvalidDataException("获取直播m3u8地址失败");
67 |
68 | return requesturl;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/Lives/LiveId.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Extensions;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace M3u8Downloader_H.bilibili.plugin.Lives
5 | {
6 | public readonly partial struct LiveId
7 | {
8 | private string Value { get; }
9 |
10 | public string Url => $"https://live.bilibili.com/{Value}";
11 |
12 | private LiveId(string value) => Value = value;
13 |
14 | public override string ToString() => Value;
15 | }
16 |
17 | public readonly partial struct LiveId
18 | {
19 | private static readonly Regex regex = new(@"live\.bilibili.*?/(\d+)(?:\?|$)", RegexOptions.Compiled);
20 | private static string? TryNormalize(string? liveIdOrUrl)
21 | {
22 | if (string.IsNullOrWhiteSpace(liveIdOrUrl))
23 | return null;
24 |
25 | var regularMatch = regex.Match(liveIdOrUrl).Groups[1].Value;
26 | if (!string.IsNullOrWhiteSpace(regularMatch))
27 | return regularMatch;
28 |
29 | return null;
30 | }
31 |
32 | public static LiveId? TryParse(string? url) => TryNormalize(url)?.Pipe(r => new LiveId(r));
33 |
34 | public static LiveId Parse(string url) => TryParse(url) ?? throw new InvalidDataException($"不能解析得地址,必须是b站直播地址:{url}");
35 |
36 | public static implicit operator LiveId(Uri liveIdOrUrl) => Parse(liveIdOrUrl.OriginalString);
37 |
38 | public static implicit operator Uri(LiveId liveId) => new(liveId.Url);
39 | }
40 |
41 | public partial struct LiveId : IEquatable
42 | {
43 | public bool Equals(LiveId other) => StringComparer.Ordinal.Equals(Value, other.Value);
44 | public override bool Equals(object? obj) => obj is LiveId other && Equals(other);
45 | public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
46 |
47 | public static bool operator ==(LiveId left, LiveId right) => left.Equals(right);
48 | public static bool operator !=(LiveId left, LiveId right) => !(left == right);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/M3u8Downloader_H.bilibili.plugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | 1.0.0.3
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/Models/LiveRoomInfos.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Utils;
2 | using Newtonsoft.Json;
3 |
4 | namespace M3u8Downloader_H.bilibili.plugin.Models
5 | {
6 | [JsonConverter(typeof(JsonPathConverter))]
7 | internal class LiveRoomInfos
8 | {
9 | [JsonProperty("roomInitRes.code")]
10 | public int Code { get; set; }
11 |
12 | [JsonProperty("roomInitRes.message")]
13 | public string Message { get; set; } = string.Empty;
14 |
15 | [JsonProperty("roomInitRes.data.playurl_info.playurl.stream")]
16 | public IList StreamInfos { get; set; } = default!;
17 | }
18 |
19 | [JsonConverter(typeof(JsonPathConverter))]
20 | internal class StreamInfo
21 | {
22 | [JsonProperty("protocol_name")]
23 | public string ProtocolName { get; set; } = string.Empty;
24 |
25 | [JsonProperty("format")]
26 | public IList CodecInfos { get; set; } = default!;
27 | }
28 |
29 | [JsonConverter(typeof(JsonPathConverter))]
30 | internal class CodecInfo
31 | {
32 | [JsonProperty("format_name")]
33 | public string FormatName { get; set; } = string.Empty;
34 |
35 | [JsonProperty("codec[0].base_url")]
36 | public string BaseUrl { get; set; } = string.Empty;
37 |
38 | [JsonProperty("codec[0].url_info[0].host")]
39 | public string HostUrl { get; set; } = string.Empty;
40 |
41 | [JsonProperty("codec[0].url_info[0].extra")]
42 | public string Extra { get; set; } = string.Empty;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.bilibili.plugin/Readers/MediaAttributeReader.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.bilibili.plugin.Extensions;
2 | using M3u8Downloader_H.Common.M3u8Infos;
3 | using M3u8Downloader_H.Plugin;
4 | using System.Web;
5 |
6 | namespace M3u8Downloader_H.bilibili.plugin.Readers
7 | {
8 | internal class MediaAttributeReader : IAttributeReader
9 | {
10 | private int currentIndex;
11 | private string CurrentIndex => $"{++currentIndex}.tmp";
12 |
13 | public bool ShouldTerminate => false;
14 |
15 | string? baseUriQuery;
16 |
17 | public MediaAttributeReader()
18 | {
19 |
20 | }
21 |
22 | public void Write(M3UFileInfo m3UFileInfo, string value, IEnumerator reader, Uri baseUri)
23 | {
24 | m3UFileInfo.MediaFiles ??= new List();
25 |
26 | var m3UmediaInfo = new M3UMediaInfo();
27 | var strArray = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
28 | if (strArray.Length > 0)
29 | m3UmediaInfo.Duration = (float?)Convert.ChangeType(strArray[0], typeof(float)) ?? 0;
30 |
31 | if (!reader.MoveNext())
32 | throw new InvalidDataException("Invalid M3U file. Missing a media URI.");
33 |
34 | string relativeUri = ChangeQuery(reader.Current.Trim(), baseUri);
35 | m3UmediaInfo.Uri = new Uri(baseUri, relativeUri);
36 | m3UmediaInfo.Title = CurrentIndex;
37 |
38 | m3UFileInfo.MediaFiles.Add(m3UmediaInfo);
39 | }
40 |
41 | private string ChangeQuery(string url,Uri baseuri)
42 | {
43 | baseUriQuery ??= HandleBaseQuery(url, baseuri);
44 | var path = url.GetPath();
45 | return $"{path}?{baseUriQuery}";
46 | }
47 |
48 | private static string HandleBaseQuery(string url,Uri baseuri)
49 | {
50 | var queryCollection = HttpUtility.ParseQueryString(baseuri.Query);
51 | var relativeUriCollection = url.GetQuery()?.GetQueryKeyPairs();
52 | if(relativeUriCollection is not null)
53 | {
54 | foreach (var item in relativeUriCollection)
55 | {
56 | queryCollection[item.Key] = item.Value;
57 | }
58 | }
59 | return queryCollection.ToString()!;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.douyin.plugin/Class1.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.douyin.plugin.Lives;
2 | using M3u8Downloader_H.Plugin;
3 |
4 | namespace M3u8Downloader_H.douyin.plugin
5 | {
6 | public class Class1 : IPluginBuilder
7 | {
8 | public IM3u8FileInfoStreamService? CreateM3U8FileInfoStreamService() => null;
9 |
10 | public IM3uFileReader? CreateM3u8FileReader() => null;
11 |
12 | public IM3u8UriProvider? CreateM3u8UriProvider() => new LiveClient();
13 |
14 | public IDownloadService? CreatePluginService() => null;
15 |
16 | public void SetAttributeReader(IAttributeReaderManager attributeReader)
17 | {
18 |
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.douyin.plugin/Lives/LiveClient.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Extensions;
2 | using M3u8Downloader_H.douyin.plugin.Models;
3 | using M3u8Downloader_H.Plugin;
4 | using Newtonsoft.Json;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace M3u8Downloader_H.douyin.plugin.Lives
8 | {
9 | internal class LiveClient : IM3u8UriProvider
10 | {
11 | private static readonly Regex regex = new(@"id=""RENDER_DATA""[\s]type=""application/json"">(.*?)", RegexOptions.Compiled);
12 | public async Task GetM3u8UriAsync(HttpClient httpClient, Uri uri, IEnumerable>? headers, CancellationToken cancellationToken = default)
13 | {
14 | if (uri.LocalPath.Contains("m3u8"))
15 | return uri;
16 |
17 | var url = await GetLiveUrl(uri, httpClient, headers, cancellationToken);
18 | return new Uri(url, UriKind.Absolute);
19 | }
20 |
21 | private static async Task GetLiveUrl(LiveId liveId,HttpClient httpClient, IEnumerable>? headers, CancellationToken cancellationToken = default)
22 | {
23 | using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, liveId);
24 | httpRequestMessage.AddHeaders(headers);
25 | if(!httpRequestMessage.Headers.Contains("Cookie"))
26 | {
27 | httpRequestMessage.Headers.Add("Cookie", "__ac_nonce=063804b7b008ae876eeb");
28 | }
29 | var raw = await httpClient.GetStringAsync(httpRequestMessage, cancellationToken);
30 | var initState = ExtractStreamInfos(raw);
31 | string decodeData = System.Web.HttpUtility.UrlDecode(initState);
32 | StreamInfos? streamInfos = JsonConvert.DeserializeObject(decodeData);
33 | if (streamInfos is null)
34 | throw new InvalidDataException("数据解析失败");
35 |
36 | if (streamInfos.Url is null)
37 | throw new InvalidDataException("没有获取到任何m3u8地址,请确认主播是否开播");
38 |
39 | return streamInfos.Url;
40 | }
41 |
42 |
43 | private static string ExtractStreamInfos(string raw)
44 | {
45 | var initState = regex.Match(raw).Groups[1].Value;
46 | if (string.IsNullOrWhiteSpace(initState))
47 | throw new InvalidDataException("没有获取到直播流得数据");
48 |
49 | return initState;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.douyin.plugin/Lives/LiveId.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Extensions;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace M3u8Downloader_H.douyin.plugin.Lives
5 | {
6 | public readonly partial struct LiveId
7 | {
8 | private string Value { get; }
9 |
10 | public string Url => $"https://live.douyin.com/{Value}";
11 |
12 | private LiveId(string value) => Value = value;
13 |
14 | public override string ToString() => Value;
15 | }
16 |
17 | public readonly partial struct LiveId
18 | {
19 | private static readonly Regex regex = new(@"live\.douyin.*?/(\d+)(?:\?|$)", RegexOptions.Compiled);
20 | private static string? TryNormalize(string? liveIdOrUrl)
21 | {
22 | if (string.IsNullOrWhiteSpace(liveIdOrUrl))
23 | return null;
24 |
25 | var regularMatch = regex.Match(liveIdOrUrl).Groups[1].Value;
26 | if (!string.IsNullOrWhiteSpace(regularMatch))
27 | return regularMatch;
28 |
29 | return null;
30 | }
31 |
32 | public static LiveId? TryParse(string? url) => TryNormalize(url)?.Pipe(r => new LiveId(r));
33 |
34 | public static LiveId Parse(string url) => TryParse(url) ?? throw new InvalidDataException("不能解析得地址,必须是抖音网页版地址,例如https://live.douyin.com/969591501540");
35 |
36 | public static implicit operator LiveId(Uri liveIdOrUrl) => Parse(liveIdOrUrl.OriginalString);
37 |
38 | public static implicit operator Uri(LiveId liveId) => new(liveId.Url);
39 | }
40 |
41 | public partial struct LiveId : IEquatable
42 | {
43 | public bool Equals(LiveId other) => StringComparer.Ordinal.Equals(Value, other.Value);
44 | public override bool Equals(object? obj) => obj is LiveId other && Equals(other);
45 | public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
46 |
47 | public static bool operator ==(LiveId left, LiveId right) => left.Equals(right);
48 | public static bool operator !=(LiveId left, LiveId right) => !(left == right);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.douyin.plugin/M3u8Downloader_H.douyin.plugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | 1.0.0.1
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.douyin.plugin/Models/StreamInfos.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Utils;
2 | using Newtonsoft.Json;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace M3u8Downloader_H.douyin.plugin.Models
10 | {
11 | [JsonConverter(typeof(JsonPathConverter))]
12 | public class StreamInfos
13 | {
14 | [JsonProperty("app.initialState.roomStore.roomInfo.room.stream_url.hls_pull_url_map.FULL_HD1")]
15 | public string Url { get; set; } = default!;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Class1.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Plugin;
2 | using M3u8Downloader_H.vlive.plugin.Readers;
3 | using M3u8Downloader_H.vlive.plugin.Streams;
4 |
5 | namespace M3u8Downloader_H.vlive.plugin
6 | {
7 | public class Class1 : IPluginBuilder
8 | {
9 | public IM3u8FileInfoStreamService? CreateM3U8FileInfoStreamService() => null;
10 |
11 | public IM3uFileReader? CreateM3u8FileReader() => new M3UFileReader();
12 |
13 | public IM3u8UriProvider? CreateM3u8UriProvider() => new StreamClient();
14 |
15 | public IDownloadService? CreatePluginService() => null;
16 |
17 | public void SetAttributeReader(IAttributeReaderManager attributeReader)
18 | {
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/M3u8Downloader_H.vlive.plugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | 1.0.0.1
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Models/InkeyInfos.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace M3u8Downloader_H.vlive.plugin.Models
9 | {
10 | internal class InkeyInfos
11 | {
12 | [JsonProperty("inkey")]
13 | public string Inkey { get; set; } = default!;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Models/PostDetail.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Utils;
2 | using Newtonsoft.Json;
3 |
4 | namespace M3u8Downloader_H.vlive.plugin.Models
5 | {
6 | [JsonConverter(typeof(JsonPathConverter))]
7 | internal class PostDetail
8 | {
9 | [JsonProperty("postDetail.post.officialVideo.liveChatId")]
10 | public string LiveChatId { get; set; } = default!;
11 |
12 | [JsonProperty("postDetail.post.officialVideo.vodId")]
13 | public string VodId { get; set; } = default!;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Models/StreamInfos.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Utils;
2 | using Newtonsoft.Json;
3 |
4 | namespace M3u8Downloader_H.vlive.plugin.Models
5 | {
6 | [JsonConverter(typeof(JsonPathConverter))]
7 | internal class StreamInfos
8 | {
9 | [JsonProperty("streams[0].keys[0].name")]
10 | public string Param { get; set; } = default!;
11 |
12 | [JsonProperty("streams[0].keys[0].value")]
13 | public string Value { get; set; } = default!;
14 |
15 | [JsonProperty("streams[0].videos")]
16 | public IList VideoInfos { get; set; } = default!;
17 | }
18 |
19 | [JsonConverter(typeof(JsonPathConverter))]
20 | internal class VideoInfo
21 | {
22 | [JsonProperty("bitrate.video")]
23 | public int Bitrate { get; set; }
24 |
25 | [JsonProperty("source")]
26 | public Uri Source { get; set; } = default!;
27 |
28 | [JsonProperty("template.body.bandwidth")]
29 | public int BandWidth { get; set; }
30 |
31 | [JsonProperty("template.body.format")]
32 | public string TsFormat { get; set; } = default!;
33 |
34 | [JsonProperty("template.body.version")]
35 | public string TsVersion { get; set; } = default!;
36 |
37 | [JsonProperty("template.body.mediaSequence")]
38 | public int MeidaSequence { get; set; } = default!;
39 |
40 | [JsonProperty("template.body.extInfos")]
41 | public int[] ExtInfos { get; set; } = default!;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Models/VodPlayInfo.cs:
--------------------------------------------------------------------------------
1 | namespace M3u8Downloader_H.vlive.plugin.Models
2 | {
3 | internal readonly struct VodPlayInfo
4 | {
5 | private readonly string Vodid;
6 | private readonly string Inkey;
7 | private static string Pid {
8 | get
9 | {
10 | TimeSpan ts = DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0);
11 | return $"rmcPlayer_{ts.Ticks}";
12 | }
13 | }
14 |
15 | public VodPlayInfo(string vodid,string inkey)
16 | {
17 | Vodid = vodid;
18 | Inkey = inkey;
19 | }
20 |
21 | public override string ToString()
22 | {
23 | return $"https://apis.naver.com/rmcnmv/rmcnmv/vod/play/v2.0/{Vodid}?key={Inkey}&pid={Pid}&sid=2024&ver=2.0&devt=html5_pc&doct=json&ptc=https&sptc=https&cpt=vtt&ctls=%7B%22visible%22%3A%7B%22fullscreen%22%3Atrue%2C%22logo%22%3Afalse%2C%22playbackRate%22%3Afalse%2C%22scrap%22%3Afalse%2C%22playCount%22%3Atrue%2C%22commentCount%22%3Atrue%2C%22title%22%3Atrue%2C%22writer%22%3Atrue%2C%22expand%22%3Atrue%2C%22subtitles%22%3Atrue%2C%22thumbnails%22%3Atrue%2C%22quality%22%3Atrue%2C%22setting%22%3Atrue%2C%22script%22%3Afalse%2C%22logoDimmed%22%3Atrue%2C%22badge%22%3Atrue%2C%22seekingTime%22%3Atrue%2C%22muted%22%3Atrue%2C%22muteButton%22%3Afalse%2C%22viewerNotice%22%3Afalse%2C%22linkCount%22%3Afalse%2C%22createTime%22%3Afalse%2C%22thumbnail%22%3Atrue%7D%2C%22clicked%22%3A%7B%22expand%22%3Afalse%2C%22subtitles%22%3Afalse%7D%7D&pv=4.26.9&dr=1920x1080&cpl=zh_CN&lc=zh_CN&adi=%5B%7B%22type%22%3A%22pre%22%2C%22exposure%22%3Afalse%2C%22replayExposure%22%3Afalse%7D%5D&adu=%2F&videoId={Vodid}&cc=CN";
24 | }
25 |
26 | public static implicit operator Uri(VodPlayInfo vodplayinfo) => new(vodplayinfo.ToString());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Readers/M3UFileReader.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.M3u8Infos;
2 | using M3u8Downloader_H.Common.Utils;
3 | using M3u8Downloader_H.Plugin;
4 | using M3u8Downloader_H.vlive.plugin.Models;
5 | using Newtonsoft.Json;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace M3u8Downloader_H.vlive.plugin.Readers
9 | {
10 | internal class M3UFileReader : IM3uFileReader
11 | {
12 | public M3UFileInfo Read(Uri baseUri, Stream stream)
13 | {
14 | StreamReader streamReader = new(stream);
15 | StreamInfos? streamInfos = JsonConvert.DeserializeObject(streamReader.ReadToEnd());
16 | if (streamInfos is null)
17 | throw new InvalidDataException("视频流反序列化失败");
18 |
19 | VideoInfo? videoInfo = streamInfos.VideoInfos
20 | .OrderByDescending(s => s.BandWidth)
21 | .FirstOrDefault();
22 | if(videoInfo is null)
23 | throw new InvalidDataException("读取视频流信息出错");
24 |
25 | var queryParam = $"?{streamInfos.Param}={streamInfos.Value}";
26 | M3UFileInfo m3UFileInfo = M3UFileInfo.CreateVodM3UFileInfo();
27 | m3UFileInfo.Version = To.Value(videoInfo.TsVersion);
28 | m3UFileInfo.MediaSequence = videoInfo.MeidaSequence;
29 | m3UFileInfo.MediaFiles = HandleMedia(videoInfo.Source, videoInfo.TsFormat, queryParam, videoInfo.ExtInfos);
30 | return m3UFileInfo;
31 | }
32 |
33 | private static IList HandleMedia(Uri baseUri,string format,string param, int[] ExtInfos)
34 | {
35 | var newFormat = Regex.Replace(format, @"%(\d+)([sd])", match => "{0:" + match.Groups[2].Value + match.Groups[1].Value + "}");
36 | int currentIndex = 0;
37 | List m3UMediaInfos = new(ExtInfos.Length);
38 | for (int i = 0; i < ExtInfos.Length; i++)
39 | {
40 | m3UMediaInfos.Add(new M3UMediaInfo()
41 | {
42 | Duration = ExtInfos[i],
43 | Uri = new Uri(baseUri, string.Format(newFormat, i) + param),
44 | Title = $"{++currentIndex}.tmp"
45 | });
46 | }
47 | return m3UMediaInfos;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Streams/StreamClient.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Extensions;
2 | using M3u8Downloader_H.Plugin;
3 | using M3u8Downloader_H.vlive.plugin.Models;
4 | using Newtonsoft.Json;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace M3u8Downloader_H.vlive.plugin.Streams
8 | {
9 | internal class StreamClient : IM3u8UriProvider
10 | {
11 | private static readonly Regex regex = new(@"__PRELOADED_STATE__=(\{.*?\}),function", RegexOptions.Compiled);
12 | public async Task GetM3u8UriAsync(HttpClient httpClient, Uri uri, IEnumerable>? headers, CancellationToken cancellationToken = default)
13 | {
14 | if (uri.LocalPath.Contains("m3u8"))
15 | return uri;
16 |
17 | PostDetail postDetail = await GetPostDetail(uri, httpClient, headers, cancellationToken);
18 | string inKey = await GetInKey(postDetail, httpClient, headers, cancellationToken);
19 | return new VodPlayInfo(postDetail.VodId, inKey);
20 | }
21 |
22 | private static async Task GetPostDetail(VideoId videoId, HttpClient httpClient, IEnumerable>? headers, CancellationToken cancellationToken = default)
23 | {
24 | var raw = await httpClient.GetStringAsync(videoId, headers, cancellationToken);
25 | var jsonRaw = ExtractInitState(raw);
26 | PostDetail? postDetail = JsonConvert.DeserializeObject(jsonRaw!);
27 | if (postDetail is null)
28 | throw new InvalidDataException("主页关键数据反序列化失败");
29 |
30 | return postDetail;
31 | }
32 |
33 | private static async Task GetInKey(PostDetail postDetail, HttpClient httpClient, IEnumerable>? headers, CancellationToken cancellationToken = default)
34 | {
35 | Uri url =new($"https://www.vlive.tv/globalv-web/vam-web/video/v1.0/vod/{postDetail.LiveChatId}/inkey?appId=8c6cc7b45d2568fb668be6e05b6e5a3b&platformType=PC&gcc=CN&locale=zh_CN");
36 | using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, url);
37 | httpRequestMessage.AddHeaders(headers);
38 | if (!httpRequestMessage.Headers.Contains("Referer"))
39 | httpRequestMessage.Headers.Add("Referer", "https://www.vlive.tv/");
40 |
41 | var raw = await httpClient.GetStringAsync(httpRequestMessage, cancellationToken);
42 | InkeyInfos? inkeyInfo = JsonConvert.DeserializeObject(raw);
43 | if (inkeyInfo is null)
44 | throw new InvalidDataException("inkey信息反序列话失败");
45 |
46 | return inkeyInfo.Inkey;
47 | }
48 |
49 | private static string ExtractInitState(string raw)
50 | {
51 | var initState = regex.Match(raw).Groups[1].Value;
52 | if (string.IsNullOrWhiteSpace(initState))
53 | throw new InvalidDataException("没有获取到网页关键数据");
54 |
55 | return initState;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/M3u8Downloader_H.vlive.plugin/Streams/VideoId.cs:
--------------------------------------------------------------------------------
1 | using M3u8Downloader_H.Common.Extensions;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace M3u8Downloader_H.vlive.plugin.Streams
5 | {
6 | public readonly partial struct VideoId
7 | {
8 | private string Value { get; }
9 |
10 | private VideoId(string value) => Value = value;
11 |
12 | public override string ToString() => Value;
13 | }
14 |
15 | public readonly partial struct VideoId
16 | {
17 | private static readonly Regex videoRegex = new(@"(?:|www\.)vlive\.tv\/video/\d+", RegexOptions.Compiled);
18 | private static readonly Regex postRegex = new(@"(?:|www\.)vlive\.tv\/post\/\d+-\d+", RegexOptions.Compiled);
19 | private static string? TryNormalize(string? url)
20 | {
21 | if (string.IsNullOrWhiteSpace(url))
22 | return null;
23 |
24 | if (videoRegex.IsMatch(url) || postRegex.IsMatch(url))
25 | return url;
26 |
27 | return null;
28 | }
29 |
30 | public static VideoId? TryParse(string? url) => TryNormalize(url)?.Pipe(r => new VideoId(r));
31 |
32 | public static VideoId Parse(string url) => TryParse(url) ?? throw new InvalidDataException("不能解析得地址,必须类似 https://www.vlive.tv/video/294312 或者 https://www.vlive.tv/post/1-31149735");
33 |
34 | public static implicit operator VideoId(Uri url) => Parse(url.OriginalString);
35 |
36 | public static implicit operator Uri(VideoId videoid) => new(videoid.ToString());
37 | }
38 |
39 | public partial struct VideoId : IEquatable
40 | {
41 | public bool Equals(VideoId other) => StringComparer.Ordinal.Equals(Value, other.Value);
42 | public override bool Equals(object? obj) => obj is VideoId other && Equals(other);
43 | public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
44 |
45 | public static bool operator ==(VideoId left, VideoId right) => left.Equals(right);
46 | public static bool operator !=(VideoId left, VideoId right) => !(left == right);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # M3u8Downloader_H.Plugins
2 | 本项目是M3u8Downloader_H项目得插件项目,此项目得目的是为了能够与主程序分离,插件更新或者升级不会影响到主程序,也就是说插件可以完成动态下载,或者动态更新。
3 |
4 | # 插件列表
5 | - 以下表格中得站点将支持直接使用视频页面得地址来进行下载
6 | - 插件还在逐步增加中
7 |
8 | |网站|插件名称|
9 | |:--:|:--:|
10 | |b站直播| M3u8Downloader_H.bilibili.plugin|
11 | |抖音直播|M3u8Downloader_H.douyin.plugin|
12 | |vlive|M3u8Downloader_H.vlive.plugin|
13 | |555影视站|M3u8Downloader_H.555dd7.plugin|
14 |
15 |
--------------------------------------------------------------------------------
/buildall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | targetDir=$1
4 | if [ ! -d $targetDir ]; then
5 | echo "${targetDir} not found!!"
6 | exit
7 | fi
8 |
9 | plugins=$(ls | grep -i "M3u8downloader_H.*.plugin$")
10 | for d in $plugins; do
11 | dotnet publish $d/ -o $d/bin/Publish -c Release
12 | zip -j $targetDir/$d.zip $d/bin/Publish/$d.dll
13 | done
14 |
--------------------------------------------------------------------------------
/datasets.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins":[
3 | {
4 | "title":"b站直播",
5 | "version":"1.0.0.3",
6 | "requiredVersion":"3.12.5",
7 | "filename":"M3u8Downloader_H.bilibili.plugin"
8 | },
9 | {
10 | "title":"抖音直播",
11 | "version":"1.0.0.1",
12 | "requiredVersion":"3.12.5",
13 | "filename":"M3u8Downloader_H.douyin.plugin"
14 | },
15 | {
16 | "title":"555影视站",
17 | "version":"1.0.0.2",
18 | "requiredVersion":"3.12.5",
19 | "filename":"M3u8Downloader_H.555dd7.plugin"
20 | },
21 | {
22 | "title":"vlive视频网站",
23 | "version":"1.0.0.1",
24 | "requiredVersion":"3.12.5",
25 | "filename":"M3u8Downloader_H.vlive.plugin"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------