├── .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 | } --------------------------------------------------------------------------------