Repository: nilaoda/N_m3u8DL-CLI Branch: master Commit: 985f6e57c33d Files: 65 Total size: 953.6 KB Directory structure: gitextract_idemaxt1/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── build_latest.yml ├── .gitignore ├── LICENSE ├── N_m3u8DL-CLI/ │ ├── App.config │ ├── CSChaCha20.cs │ ├── Decode51CtoKey.cs │ ├── DecodeCdeledu.cs │ ├── DecodeDdyun.cs │ ├── DecodeHuke88Key.cs │ ├── DecodeImooc.cs │ ├── DecodeNfmovies.cs │ ├── Decrypter.cs │ ├── DownloadManager.cs │ ├── Downloader.cs │ ├── FFmpeg.cs │ ├── Global.cs │ ├── HLSLiveDownloader.cs │ ├── HLSTags.cs │ ├── IqJsonParser.cs │ ├── LOGGER.cs │ ├── MPDParser.cs │ ├── MyOptions.cs │ ├── N_m3u8DL-CLI.csproj │ ├── Parser.cs │ ├── Program.cs │ ├── ProgressReporter.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Watcher.cs │ ├── changelog.txt │ ├── packages.config │ ├── strings.Designer.cs │ ├── strings.en-US.Designer.cs │ ├── strings.en-US.resx │ ├── strings.resx │ ├── strings.zh-TW.Designer.cs │ └── strings.zh-TW.resx ├── N_m3u8DL-CLI.sln ├── README.md ├── README_ENG.md └── docs/ ├── Advanced.html ├── GetM3u8.html ├── Introductory.html ├── M3U8URL2File.html ├── SimpleGUI.html ├── gitbook/ │ ├── fonts/ │ │ └── fontawesome/ │ │ └── FontAwesome.otf │ ├── gitbook-plugin-donate/ │ │ ├── plugin.css │ │ └── plugin.js │ ├── gitbook-plugin-fontsettings/ │ │ ├── fontsettings.js │ │ └── website.css │ ├── gitbook-plugin-github/ │ │ └── plugin.js │ ├── gitbook-plugin-github-buttons/ │ │ └── plugin.js │ ├── gitbook-plugin-highlight/ │ │ ├── ebook.css │ │ └── website.css │ ├── gitbook-plugin-lunr/ │ │ └── search-lunr.js │ ├── gitbook-plugin-search/ │ │ ├── search-engine.js │ │ ├── search.css │ │ └── search.js │ ├── gitbook-plugin-sharing-plus/ │ │ └── buttons.js │ ├── gitbook.js │ ├── style.css │ └── theme.js ├── index.html └── search_index.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: ['https://nilaoda.github.io/N_m3u8DL-CLI/source/images/alipay.png','https://www.buymeacoffee.com/nilaoda'] ================================================ FILE: .github/workflows/build_latest.yml ================================================ name: Build_Latest on: [push] jobs: build: runs-on: windows-latest steps: - uses: actions/checkout@v1 name: Checkout Code - name: Setup MSBuild Path uses: warrenbuckley/Setup-MSBuild@v1 env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - name: Setup NuGet uses: NuGet/setup-nuget@v1.0.2 env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - name: Restore NuGet Packages run: nuget restore N_m3u8DL-CLI.sln - name: Build run: msbuild N_m3u8DL-CLI.sln /p:Configuration=Release /p:DebugSymbols=false /p:DebugType=None - name: Upload Artifact uses: actions/upload-artifact@v1.0.0 with: name: N_m3u8DL-CLI_latest path: N_m3u8DL-CLI\bin\Release\N_m3u8DL-CLI.exe ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted #*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 nilaoda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: N_m3u8DL-CLI/App.config ================================================ ================================================ FILE: N_m3u8DL-CLI/CSChaCha20.cs ================================================ /* * Copyright (c) 2015, 2018 Scott Bennett * (c) 2018-2021 Kaarlo Räihä * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ using System; using System.IO; using System.Text; using System.Threading.Tasks; using System.Runtime.CompilerServices; // For MethodImplOptions.AggressiveInlining namespace CSChaCha20 { /// /// Class that can be used for ChaCha20 encryption / decryption /// public sealed class ChaCha20 : IDisposable { /// /// Only allowed key lenght in bytes /// public const int allowedKeyLength = 32; /// /// Only allowed nonce lenght in bytes /// public const int allowedNonceLength = 12; /// /// How many bytes are processed per loop /// public const int processBytesAtTime = 64; private const int stateLength = 16; /// /// The ChaCha20 state (aka "context") /// private readonly uint[] state = new uint[stateLength]; /// /// Determines if the objects in this class have been disposed of. Set to true by the Dispose() method. /// private bool isDisposed = false; /// /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens. /// /// /// See ChaCha20 Spec Section 2.4 for a detailed description of the inputs. /// /// /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers /// /// /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers /// /// /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer /// public ChaCha20(byte[] key, byte[] nonce, uint counter) { this.KeySetup(key); this.IvSetup(nonce, counter); } #if NET6_0_OR_GREATER /// /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens. /// /// /// See ChaCha20 Spec Section 2.4 for a detailed description of the inputs. /// /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer public ChaCha20(ReadOnlySpan key, ReadOnlySpan nonce, uint counter) { this.KeySetup(key.ToArray()); this.IvSetup(nonce.ToArray(), counter); } #endif // NET6_0_OR_GREATER /// /// The ChaCha20 state (aka "context"). Read-Only. /// public uint[] State { get { return this.state; } } // These are the same constants defined in the reference implementation. // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c private static readonly byte[] sigma = Encoding.ASCII.GetBytes("expand 32-byte k"); private static readonly byte[] tau = Encoding.ASCII.GetBytes("expand 16-byte k"); /// /// Set up the ChaCha state with the given key. A 32-byte key is required and enforced. /// /// /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers /// private void KeySetup(byte[] key) { if (key == null) { throw new ArgumentNullException("Key is null"); } if (key.Length != allowedKeyLength) { throw new ArgumentException($"Key length must be {allowedKeyLength}. Actual: {key.Length}"); } state[4] = Util.U8To32Little(key, 0); state[5] = Util.U8To32Little(key, 4); state[6] = Util.U8To32Little(key, 8); state[7] = Util.U8To32Little(key, 12); byte[] constants = (key.Length == allowedKeyLength) ? sigma : tau; int keyIndex = key.Length - 16; state[8] = Util.U8To32Little(key, keyIndex + 0); state[9] = Util.U8To32Little(key, keyIndex + 4); state[10] = Util.U8To32Little(key, keyIndex + 8); state[11] = Util.U8To32Little(key, keyIndex + 12); state[0] = Util.U8To32Little(constants, 0); state[1] = Util.U8To32Little(constants, 4); state[2] = Util.U8To32Little(constants, 8); state[3] = Util.U8To32Little(constants, 12); } /// /// Set up the ChaCha state with the given nonce (aka Initialization Vector or IV) and block counter. A 12-byte nonce and a 4-byte counter are required. /// /// /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers /// /// /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer /// private void IvSetup(byte[] nonce, uint counter) { if (nonce == null) { // There has already been some state set up. Clear it before exiting. Dispose(); throw new ArgumentNullException("Nonce is null"); } if (nonce.Length != allowedNonceLength) { // There has already been some state set up. Clear it before exiting. Dispose(); throw new ArgumentException($"Nonce length must be {allowedNonceLength}. Actual: {nonce.Length}"); } state[12] = counter; state[13] = Util.U8To32Little(nonce, 0); state[14] = Util.U8To32Little(nonce, 4); state[15] = Util.U8To32Little(nonce, 8); } #region Encryption methods /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array /// Number of bytes to encrypt public void EncryptBytes(byte[] output, byte[] input, int numBytes) { this.WorkBytes(output, input, numBytes); } /// /// Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 public void EncryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) { this.WorkStreams(output, input, howManyBytesToProcessAtTime); } /// /// Async encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 public async Task EncryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) { await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime); } /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array public void EncryptBytes(byte[] output, byte[] input) { this.WorkBytes(output, input, input.Length); } /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Number of bytes to encrypt /// Byte array that contains encrypted bytes public byte[] EncryptBytes(byte[] input, int numBytes) { byte[] returnArray = new byte[numBytes]; this.WorkBytes(returnArray, input, numBytes); return returnArray; } /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Byte array that contains encrypted bytes public byte[] EncryptBytes(byte[] input) { byte[] returnArray = new byte[input.Length]; this.WorkBytes(returnArray, input, input.Length); return returnArray; } /// /// Encrypt string as UTF8 byte array, returns byte array that is allocated by method. /// /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform /// Input string /// Byte array that contains encrypted bytes public byte[] EncryptString(string input) { byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input); byte[] returnArray = new byte[utf8Bytes.Length]; this.WorkBytes(returnArray, utf8Bytes, utf8Bytes.Length); return returnArray; } #endregion // Encryption methods #region // Decryption methods /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to the output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array /// Input byte array /// Number of bytes to decrypt public void DecryptBytes(byte[] output, byte[] input, int numBytes) { this.WorkBytes(output, input, numBytes); } /// /// Decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 public void DecryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) { this.WorkStreams(output, input, howManyBytesToProcessAtTime); } /// /// Async decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 public async Task DecryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) { await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime); } /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array public void DecryptBytes(byte[] output, byte[] input) { WorkBytes(output, input, input.Length); } /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Number of bytes to encrypt /// Byte array that contains decrypted bytes public byte[] DecryptBytes(byte[] input, int numBytes) { byte[] returnArray = new byte[numBytes]; WorkBytes(returnArray, input, numBytes); return returnArray; } /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Byte array that contains decrypted bytes public byte[] DecryptBytes(byte[] input) { byte[] returnArray = new byte[input.Length]; WorkBytes(returnArray, input, input.Length); return returnArray; } /// /// Decrypt UTF8 byte array to string. /// /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform /// Byte array /// Byte array that contains encrypted bytes public string DecryptUTF8ByteArray(byte[] input) { byte[] tempArray = new byte[input.Length]; WorkBytes(tempArray, input, input.Length); return System.Text.Encoding.UTF8.GetString(tempArray); } #endregion // Decryption methods private void WorkStreams(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) { int readBytes; byte[] inputBuffer = new byte[howManyBytesToProcessAtTime]; byte[] outputBuffer = new byte[howManyBytesToProcessAtTime]; while ((readBytes = input.Read(inputBuffer, 0, howManyBytesToProcessAtTime)) > 0) { // Encrypt or decrypt WorkBytes(output: outputBuffer, input: inputBuffer, numBytes: readBytes); // Write buffer output.Write(outputBuffer, 0, readBytes); } } private async Task WorkStreamsAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) { byte[] readBytesBuffer = new byte[howManyBytesToProcessAtTime]; byte[] writeBytesBuffer = new byte[howManyBytesToProcessAtTime]; int howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime); while (howManyBytesWereRead > 0) { // Encrypt or decrypt WorkBytes(output: writeBytesBuffer, input: readBytesBuffer, numBytes: howManyBytesWereRead); // Write await output.WriteAsync(writeBytesBuffer, 0, howManyBytesWereRead); // Read more howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime); } } /// /// Encrypt or decrypt an arbitrary-length byte array (input), writing the resulting byte array to the output buffer. The number of bytes to read from the input buffer is determined by numBytes. /// /// Output byte array /// Input byte array /// How many bytes to process private void WorkBytes(byte[] output, byte[] input, int numBytes) { if (isDisposed) { throw new ObjectDisposedException("state", "The ChaCha state has been disposed"); } if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (output == null) { throw new ArgumentNullException("output", "Output cannot be null"); } if (numBytes < 0 || numBytes > input.Length) { throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]"); } if (output.Length < numBytes) { throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}"); } uint[] x = new uint[stateLength]; // Working buffer byte[] tmp = new byte[processBytesAtTime]; // Temporary buffer int offset = 0; while (numBytes > 0) { // Copy state to working buffer Buffer.BlockCopy(this.state, 0, x, 0, stateLength * sizeof(uint)); for (int i = 0; i < 10; i++) { QuarterRound(x, 0, 4, 8, 12); QuarterRound(x, 1, 5, 9, 13); QuarterRound(x, 2, 6, 10, 14); QuarterRound(x, 3, 7, 11, 15); QuarterRound(x, 0, 5, 10, 15); QuarterRound(x, 1, 6, 11, 12); QuarterRound(x, 2, 7, 8, 13); QuarterRound(x, 3, 4, 9, 14); } for (int i = 0; i < stateLength; i++) { Util.ToBytes(tmp, Util.Add(x[i], this.state[i]), 4 * i); } this.state[12] = Util.AddOne(state[12]); if (this.state[12] <= 0) { /* Stopping at 2^70 bytes per nonce is the user's responsibility */ this.state[13] = Util.AddOne(state[13]); } // In case these are last bytes if (numBytes <= processBytesAtTime) { for (int i = 0; i < numBytes; i++) { output[i + offset] = (byte)(input[i + offset] ^ tmp[i]); } return; } for (int i = 0; i < processBytesAtTime; i++) { output[i + offset] = (byte)(input[i + offset] ^ tmp[i]); } numBytes -= processBytesAtTime; offset += processBytesAtTime; } } /// /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned integers within the given buffer at indices a, b, c, and d. /// /// /// The ChaCha state does not have four integer numbers: it has 16. So the quarter-round operation works on only four of them -- hence the name. Each quarter round operates on four predetermined numbers in the ChaCha state. /// See ChaCha20 Spec Sections 2.1 - 2.2. /// /// A ChaCha state (vector). Must contain 16 elements. /// Index of the first number /// Index of the second number /// Index of the third number /// Index of the fourth number private static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d) { x[a] = Util.Add(x[a], x[b]); x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16); x[c] = Util.Add(x[c], x[d]); x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12); x[a] = Util.Add(x[a], x[b]); x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8); x[c] = Util.Add(x[c], x[d]); x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7); } #region Destructor and Disposer /// /// Clear and dispose of the internal state. The finalizer is only called if Dispose() was never called on this cipher. /// ~ChaCha20() { Dispose(false); } /// /// Clear and dispose of the internal state. Also request the GC not to call the finalizer, because all cleanup has been taken care of. /// public void Dispose() { Dispose(true); /* * The Garbage Collector does not need to invoke the finalizer because Dispose(bool) has already done all the cleanup needed. */ GC.SuppressFinalize(this); } /// /// This method should only be invoked from Dispose() or the finalizer. This handles the actual cleanup of the resources. /// /// /// Should be true if called by Dispose(); false if called by the finalizer /// private void Dispose(bool disposing) { if (!isDisposed) { if (disposing) { /* Cleanup managed objects by calling their Dispose() methods */ } /* Cleanup any unmanaged objects here */ Array.Clear(state, 0, stateLength); } isDisposed = true; } #endregion // Destructor and Disposer } /// /// Utilities that are used during compression /// public static class Util { /// /// n-bit left rotation operation (towards the high bits) for 32-bit integers. /// /// /// /// The result of (v LEFTSHIFT c) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Rotate(uint v, int c) { unchecked { return (v << c) | (v >> (32 - c)); } } /// /// Unchecked integer exclusive or (XOR) operation. /// /// /// /// The result of (v XOR w) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint XOr(uint v, uint w) { return unchecked(v ^ w); } /// /// Unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32. /// /// /// See ChaCha20 Spec Section 2.1. /// /// /// /// The result of (v + w) modulo 2^32 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Add(uint v, uint w) { return unchecked(v + w); } /// /// Add 1 to the input parameter using unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32. /// /// /// See ChaCha20 Spec Section 2.1. /// /// /// The result of (v + 1) modulo 2^32 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint AddOne(uint v) { return unchecked(v + 1); } /// /// Convert four bytes of the input buffer into an unsigned 32-bit integer, beginning at the inputOffset. /// /// /// /// An unsigned 32-bit integer [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint U8To32Little(byte[] p, int inputOffset) { unchecked { return ((uint)p[inputOffset] | ((uint)p[inputOffset + 1] << 8) | ((uint)p[inputOffset + 2] << 16) | ((uint)p[inputOffset + 3] << 24)); } } /// /// Serialize the input integer into the output buffer. The input integer will be split into 4 bytes and put into four sequential places in the output buffer, starting at the outputOffset. /// /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ToBytes(byte[] output, uint input, int outputOffset) { unchecked { output[outputOffset] = (byte)input; output[outputOffset + 1] = (byte)(input >> 8); output[outputOffset + 2] = (byte)(input >> 16); output[outputOffset + 3] = (byte)(input >> 24); } } } } ================================================ FILE: N_m3u8DL-CLI/Decode51CtoKey.cs ================================================ using NiL.JS.BaseLibrary; using NiL.JS.Core; using NiL.JS.Extensions; using System.Security.Cryptography; using System.Text; namespace N_m3u8DL_CLI { /* * js代码来自:https://static1.51ctocdn.cn/edu/player/h5/h5player.js line:9421 * */ class Decode51CtoKey { private static string JS = @" var Base64={_keyStr:'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',encode:function(e){var t='';var n,r,i,s,o,u,a;var f=0;e=Base64._utf8_encode(e);while(f>2;o=(n&3)<<4|r>>4;u=(r&15)<<2|i>>6;a=i&63;if(isNaN(r)){u=a=64}else if(isNaN(i)){a=64}t=t+this._keyStr.charAt(s)+this._keyStr.charAt(o)+this._keyStr.charAt(u)+this._keyStr.charAt(a)}return t},decode:function(e){var t='';var n,r,i;var s,o,u,a;var f=0;e=e.replace(/[^A-Za-z0-9\+\/\=]/g,'');while(f>4;r=(o&15)<<4|u>>2;i=(u&3)<<6|a;t=t+String.fromCharCode(n);if(u!=64){t=t+String.fromCharCode(r)}if(a!=64){t=t+String.fromCharCode(i)}}t=Base64._utf8_decode(t);return t},_utf8_encode:function(e){e=e.replace(/\r\n/g,'\n');var t='';for(var n=0;n127&&r<2048){t+=String.fromCharCode(r>>6|192);t+=String.fromCharCode(r&63|128)}else{t+=String.fromCharCode(r>>12|224);t+=String.fromCharCode(r>>6&63|128);t+=String.fromCharCode(r&63|128)}}return t},_utf8_decode:function(e){var t='';var n=0;var r=c1=c2=0;while(n191&&r<224){c2=e.charCodeAt(n+1);t+=String.fromCharCode((r&31)<<6|c2&63);n+=2}else{c2=e.charCodeAt(n+1);c3=e.charCodeAt(n+2);t+=String.fromCharCode((r&15)<<12|(c2&63)<<6|c3&63);n+=3}}return t}} var btoa = function(str) { return Base64.encode(str); } var bu = function (e, t) { for (var r = t - (e += '').length; 0 < r; r--) e = '0' + e; return e } , MD5 = function (e) { var t, a = 0, n = 8; function o(e, t, r, i, a, n) { return g((s = g(g(t, e), g(i, n))) << a | s >>> 32 - a, r); var s } function c(e, t, r, i, a, n, s) { return o(t & r | ~t & i, e, t, a, n, s) } function h(e, t, r, i, a, n, s) { return o(t & i | r & ~i, e, t, a, n, s) } function f(e, t, r, i, a, n, s) { return o(t ^ r ^ i, e, t, a, n, s) } function p(e, t, r, i, a, n, s) { return o(r ^ (t | ~i), e, t, a, n, s) } function g(e, t) { var r = (65535 & e) + (65535 & t); return (e >> 16) + (t >> 16) + (r >> 16) << 16 | 65535 & r } return function (e) { for (var t = a ? '0123456789ABCDEF' : '0123456789abcdef', r = '', i = 0; i < 4 * e.length; i++) r += t.charAt(e[i >> 2] >> i % 4 * 8 + 4 & 15) + t.charAt(e[i >> 2] >> i % 4 * 8 & 15); return r }(function (e, t) { e[t >> 5] = e[t >> 5] | (128 << t % 32), e[14 + (t + 64 >>> 9 << 4)] = t; for (var r = 1732584193, i = -271733879, a = -1732584194, n = 271733878, s = 0; s < e.length; s += 16) { var o = r , l = i , u = a , d = n; i = p(i = p(i = p(i = p(i = f(i = f(i = f(i = f(i = h(i = h(i = h(i = h(i = c(i = c(i = c(i = c(i, a = c(a, n = c(n, r = c(r, i, a, n, e[s + 0], 7, -680876936), i, a, e[s + 1], 12, -389564586), r, i, e[s + 2], 17, 606105819), n, r, e[s + 3], 22, -1044525330), a = c(a, n = c(n, r = c(r, i, a, n, e[s + 4], 7, -176418897), i, a, e[s + 5], 12, 1200080426), r, i, e[s + 6], 17, -1473231341), n, r, e[s + 7], 22, -45705983), a = c(a, n = c(n, r = c(r, i, a, n, e[s + 8], 7, 1770035416), i, a, e[s + 9], 12, -1958414417), r, i, e[s + 10], 17, -42063), n, r, e[s + 11], 22, -1990404162), a = c(a, n = c(n, r = c(r, i, a, n, e[s + 12], 7, 1804603682), i, a, e[s + 13], 12, -40341101), r, i, e[s + 14], 17, -1502002290), n, r, e[s + 15], 22, 1236535329), a = h(a, n = h(n, r = h(r, i, a, n, e[s + 1], 5, -165796510), i, a, e[s + 6], 9, -1069501632), r, i, e[s + 11], 14, 643717713), n, r, e[s + 0], 20, -373897302), a = h(a, n = h(n, r = h(r, i, a, n, e[s + 5], 5, -701558691), i, a, e[s + 10], 9, 38016083), r, i, e[s + 15], 14, -660478335), n, r, e[s + 4], 20, -405537848), a = h(a, n = h(n, r = h(r, i, a, n, e[s + 9], 5, 568446438), i, a, e[s + 14], 9, -1019803690), r, i, e[s + 3], 14, -187363961), n, r, e[s + 8], 20, 1163531501), a = h(a, n = h(n, r = h(r, i, a, n, e[s + 13], 5, -1444681467), i, a, e[s + 2], 9, -51403784), r, i, e[s + 7], 14, 1735328473), n, r, e[s + 12], 20, -1926607734), a = f(a, n = f(n, r = f(r, i, a, n, e[s + 5], 4, -378558), i, a, e[s + 8], 11, -2022574463), r, i, e[s + 11], 16, 1839030562), n, r, e[s + 14], 23, -35309556), a = f(a, n = f(n, r = f(r, i, a, n, e[s + 1], 4, -1530992060), i, a, e[s + 4], 11, 1272893353), r, i, e[s + 7], 16, -155497632), n, r, e[s + 10], 23, -1094730640), a = f(a, n = f(n, r = f(r, i, a, n, e[s + 13], 4, 681279174), i, a, e[s + 0], 11, -358537222), r, i, e[s + 3], 16, -722521979), n, r, e[s + 6], 23, 76029189), a = f(a, n = f(n, r = f(r, i, a, n, e[s + 9], 4, -640364487), i, a, e[s + 12], 11, -421815835), r, i, e[s + 15], 16, 530742520), n, r, e[s + 2], 23, -995338651), a = p(a, n = p(n, r = p(r, i, a, n, e[s + 0], 6, -198630844), i, a, e[s + 7], 10, 1126891415), r, i, e[s + 14], 15, -1416354905), n, r, e[s + 5], 21, -57434055), a = p(a, n = p(n, r = p(r, i, a, n, e[s + 12], 6, 1700485571), i, a, e[s + 3], 10, -1894986606), r, i, e[s + 10], 15, -1051523), n, r, e[s + 1], 21, -2054922799), a = p(a, n = p(n, r = p(r, i, a, n, e[s + 8], 6, 1873313359), i, a, e[s + 15], 10, -30611744), r, i, e[s + 6], 15, -1560198380), n, r, e[s + 13], 21, 1309151649), a = p(a, n = p(n, r = p(r, i, a, n, e[s + 4], 6, -145523070), i, a, e[s + 11], 10, -1120210379), r, i, e[s + 2], 15, 718787259), n, r, e[s + 9], 21, -343485551), r = g(r, o), i = g(i, l), a = g(a, u), n = g(n, d) } return Array(r, i, a, n) }(function (e) { for (var t = Array(), r = (1 << n) - 1, i = 0; i < e.length * n; i += n) t[i >> 5] = t[i >> 5] | ((e.charCodeAt(i / n) & r) << i % 32); return t }(t = e), t.length * n)) } , eeb64 = function (e) { for (var t = '', r = '', i = 0; i < e.length; i++) t += bu('BqrCwxVefD9457mnoHINOPQRSUXLMabFcdghijyzkl6GApstuJKvW0YZ23ET81=_'.indexOf(e[i]).toString(2), 6); for (t = t.substring(t.length % 8), i = 0; i < Math.ceil(t.length / 8); i++) r += String.fromCharCode(parseInt(t.substr(8 * i, 8), 2)); return base64decode(r) } , dec = function (e, t) { function r(e) { for (var t = 0; t < s.length; t++) if (s[t] == e) return t } e[1]; var i = [o[r(e[13])], o[r(e[8])], o[r(e[4])]] , a = e.substr(0, 1) + e.substr(2, 2) + e.substr(5, 3) + e.substr(9, 4) + e.substr(14); debugger;var x = [r(e[13]),r(e[8]),r(e[4])] for (var n in i) a = i[n](a, t); return a } , base64decode = function (e) { var t, r, i, a, n, s, o, l = new Array(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1); for (s = e.length, n = 0, o = ''; n < s;) { for (; t = l[255 & e.charCodeAt(n++)], n < s && -1 == t;) ; if (-1 == t) break; for (; r = l[255 & e.charCodeAt(n++)], n < s && -1 == r;) ; if (-1 == r) break; o += String.fromCharCode(t << 2 | (48 & r) >> 4); do { if (61 == (i = 255 & e.charCodeAt(n++))) return o; i = l[i] } while (n < s && -1 == i); if (-1 == i) break; o += String.fromCharCode((15 & r) << 4 | (60 & i) >> 2); do { if (61 == (a = 255 & e.charCodeAt(n++))) return o; a = l[a] } while (n < s && -1 == a); if (-1 == a) break; o += String.fromCharCode((3 & i) << 6 | a) } return o } , base64ToArrayBuffer = function (e) { for (var t = atob(e), r = t.length, i = new Uint8Array(r), a = 0; a < r; a++) i[a] = t.charCodeAt(a); return i.buffer } , arrayBufferToBase64 = function (e) { for (var t = '', r = new Uint8Array(e), i = r.byteLength, a = 0; a < i; a++) t += String.fromCharCode(r[a]); return btoa(t) }; var s = ['s', 'i', 'y', 'u', 'a', 'n', 't', 'l', 'w', 'x'] , o = [function (e) { return e } , function (e, t, r) { r = r || 'eDu_51Cto_siyuanTlw'; for (var i = base64decode(e).split(''), a = MD5(t + r).toString(), n = a.length - 1; 0 <= n; n--) { var s = a[n].charCodeAt() % (i.length - 1); i.splice(s, 1) } return i.join('') } , function (e, t, r) { for (var i = t % 7, a = e.length, n = '', s = 0; s < a / 2; s++) { var o = 2 * s; n += 0 == i || s % i == 0 ? e[o] + e[o + 1] : e[o + 1] ? e[o + 1] + e[o] : e[o] } var l = base64decode(n) , u = (l.length - 1) / 2 , d = ''; for (s = 0; s < u; s++) o = 2 * s, i < s && o++ , d += s % 3 == 0 ? l[o] : l[o + 1]; return d } , function (e) { return e } , function (e) { return e } , function (e, t, r) { var i, a, n, s, o, l, u, d = e.slice(0, 7) + e.slice(10, 12) + e.slice(15, -3), c = '', h = 0, f = 0, p = ''; d = d.split('').reverse().join(''), i = eeb64(d), a = parseInt(i.substr(0, 1)), s = (n = i.slice(6, -3)).match(/^\d*/), o = n.match(/\d*$/), l = s[0], u = o[0], n = n.replace(/^\d*/, '').replace(/\d*$/, ''); for (var g = 0; g < u.length; g++) c += bu(parseInt(u[g]).toString(2), 3); for (c = c.substr(a), g = 0; g < c.length; g++) 1 == c[g] ? (p += l[f], f++) : (p += n[h], h++); return p } , function (e, t, r) { for (var i, a = { B: '0', q: '1', r: '2', C: '3', w: '4', x: '5', V: '6', e: '7', f: '8', D: '9', 9: 'a', 4: 'b', 5: 'c', 7: 'd', m: 'e', n: 'f', o: 'g', H: 'h', I: 'i', N: 'j', O: 'k', P: 'l', Q: 'm', R: 'n', S: 'o', U: 'p', X: 'q', L: 'r', M: 's', a: 't', b: 'u', F: 'v', c: 'w', d: 'x', g: 'y', h: 'z', i: 'A', j: 'B', y: 'C', z: 'D', k: 'E', l: 'F', 6: 'G', G: 'H', A: 'I', p: 'J', s: 'K', t: 'L', u: 'M', J: 'N', K: 'O', v: 'P', W: 'Q', 0: 'R', Y: 'S', Z: 'T', 2: 'U', 3: 'V', E: 'W', T: 'X', 8: 'Y', 1: 'Z' }, n = 5, s = '', o = 0, l = '', u = 0, d = 0; d < e.length; d++) { var c = e[d]; s += a[c] ? a[c] : c } for (d = 0; d < 8; d++) i = 7 == d ? 32 - u : Math.abs(8 - n++), l += s.substr(o++, 1), o += i, u += i; return l += s.substr(40), eeb64(l.split('').reverse().join('')) } , function (e, t, r) { r = r || 'eDu_51Cto_siyuanTlw'; var i = eeb64(e) , a = MD5(r + t).toString().slice(0, 16) , n = i.indexOf(a) , s = parseInt(i.slice(0, n), 16); if (!n) return !1; var o = i.substr(16 + n); return o.length == s && o } ]; function getKey(text, lid) { return btoa(dec(text, lid)); }"; private static string MD5Encoding(string rawPass) { MD5 md5 = MD5.Create(); byte[] bs = Encoding.UTF8.GetBytes(rawPass); byte[] hs = md5.ComputeHash(bs); StringBuilder sb = new StringBuilder(); foreach (byte b in hs) { sb.Append(b.ToString("x2")); } return sb.ToString(); } public static string GetDecodeKey(string encodeKey, string lid) { var context = new Context(); context.Eval(JS); var concatFunction = context.GetVariable("getKey").As(); string key = concatFunction.Call(new Arguments { encodeKey, lid }).ToString(); return key; } public static string GetSign(string lid) { var data = lid + "eDu_51Cto_siyuanTlw"; return MD5Encoding(data); } } } ================================================ FILE: N_m3u8DL-CLI/DecodeCdeledu.cs ================================================ using NiL.JS.BaseLibrary; using NiL.JS.Core; using NiL.JS.Extensions; using System; using Array = System.Array; namespace N_m3u8DL_CLI { internal class DecodeCdeledu { private static string JS = @" var _keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; var removePaddingChars = function(input) { var lkey = _keyStr.indexOf(input.charAt(input.length - 1)); if (lkey == 64) { return input.substring(0, input.length - 1); } return input; } var base64Decode = function(input, arrayBuffer) { input = removePaddingChars(input); input = removePaddingChars(input); var bytes = parseInt((input.length / 4) * 3, 10); var uarray; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; var j = 0; if (arrayBuffer) { uarray = new Uint8Array(arrayBuffer); } else { uarray = new Uint8Array(bytes); } input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); for (i = 0; i < bytes; i += 3) { enc1 = _keyStr.indexOf(input.charAt(j++)); enc2 = _keyStr.indexOf(input.charAt(j++)); enc3 = _keyStr.indexOf(input.charAt(j++)); enc4 = _keyStr.indexOf(input.charAt(j++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; uarray[i] = chr1; if (enc3 != 64) uarray[i + 1] = chr2; if (enc4 != 64) uarray[i + 2] = chr3; } return uarray; } var uint8ArrayToString = function(uDataArr) { var arrStr = ''; for (var i = 0; i < uDataArr.length; i++) { arrStr += String.fromCharCode(uDataArr[i]); } return arrStr; } var decodeKey = function(dataKeyString) { var decodeArr = base64Decode(dataKeyString); var decodeArrString = uint8ArrayToString(decodeArr); return decodeArrString; if (decodeArrString.indexOf('|&|') > 0) { return decodeArrString; } return ''; } "; //https://video.cdeledu.com/js/lib/cdel.hls.min-1.0.js?v=1.3 public static string DecodeKey(string txt) { var context = new Context(); context.Eval(JS); var concatFunction = context.GetVariable("decodeKey").As(); string key = concatFunction.Call(new Arguments { txt }).ToString(); string realKey = key.Split(new string[] { "|&|" }, StringSplitOptions.None)[1]; return realKey; } } } ================================================ FILE: N_m3u8DL-CLI/DecodeDdyun.cs ================================================ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace N_m3u8DL_CLI { class DecodeDdyun { public static string DecryptM3u8(byte[] byteArray) { string tmp = DecodeNfmovies.DecryptM3u8(byteArray); if (tmp.StartsWith("duoduo.key")) { tmp = Regex.Replace(tmp, @"#EXT-X-BYTERANGE:.*\s", ""); tmp = tmp.Replace("https:", "jump/https:") .Replace("inews.gtimg.com", "puui.qpic.cn"); } return tmp; } //https://player.ddyunp.com/jQuery.min.js?v1.5 public static string GetVaildM3u8Url(string url) { //url: https://hls.ddyunp.com/ddyun/id/1/key/playlist.m3u8 string id = Regex.Match(url, @"\w{20,}").Value; string tm = Global.GetTimeStamp(false); string t = ((long.Parse(tm) / 0x186a0) * 0x64).ToString(); string tmp = id + "duoduo" + "1" + t; MD5 md5 = MD5.Create(); byte[] bs = Encoding.UTF8.GetBytes(tmp); byte[] hs = md5.ComputeHash(bs); StringBuilder sb = new StringBuilder(); foreach (byte b in hs) { sb.Append(b.ToString("x2")); } string key = sb.ToString(); return Regex.Replace(url, @"1/\w{20,}", "1/" + key); } } } ================================================ FILE: N_m3u8DL-CLI/DecodeHuke88Key.cs ================================================ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace N_m3u8DL_CLI { //https://js.huke88.com/assets/revision/js/plugins/tcplayer/tcplayer.v4.1.min.js?v=930 //https://js.huke88.com/assets/revision/js/plugins/tcplayer/libs/hls.min.0.13.2m.js?v=930 class DecodeHuke88Key { private static string[] GetOverlayInfo(string url) { var enc = new Regex("eyJ\\w{100,}").Match(url).Value; var json = Encoding.UTF8.GetString(Convert.FromBase64String(enc)); JObject jObject = JObject.Parse(json); var key = jObject["overlayKey"].ToString(); var iv = jObject["overlayIv"].ToString(); return new string[] { key, iv }; } public static string DecodeKey(string url, byte[] data) { var info = GetOverlayInfo(url); var overlayKey = info[0]; var overlayIv = info[1]; var l = new List(); var c = new List(); for (int h = 0; h < 16; h++) { var f = overlayKey.Substring(2 * h, 2); var g = overlayIv.Substring(2 * h, 2); l.Add(Convert.ToByte(f, 16)); c.Add(Convert.ToByte(g, 16)); } var _lastCipherblock = c.ToArray(); var t = new byte[data.Length]; var r = data; r = Decrypter.AES128Decrypt(data, l.ToArray(), Decrypter.HexStringToBytes("00000000000000000000000000000000"), CipherMode.CBC, PaddingMode.Zeros); for (var o = 0; o < 16; o++) t[o] = (byte)(r[o] ^ _lastCipherblock[o]); var key = Convert.ToBase64String(t); return key; } } } ================================================ FILE: N_m3u8DL-CLI/DecodeImooc.cs ================================================ using NiL.JS.BaseLibrary; using NiL.JS.Core; using NiL.JS.Extensions; using System; using Array = System.Array; namespace N_m3u8DL_CLI { /* * js代码来自:https://www.imooc.com/static/moco/player/3.0.6.3/mocoplayer.js?v=202006122046 * */ class DecodeImooc { private static string JS = @" function n(t, e) { function r(t, e) { var r = ''; if ('object' == typeof t) for (var n = 0; n < t.length; n++) r += String.fromCharCode(t[n]); t = r || t; for (var i, o, a = new Uint8Array(t.length), s = e.length, n = 0; n < t.length; n++) o = n % s, i = t[n], i = i.toString().charCodeAt(0), a[n] = i ^ e.charCodeAt(o); return a } function n(t) { var e = ''; if ('object' == typeof t) for (var r = 0; r < t.length; r++) e += String.fromCharCode(t[r]); t = e || t; var n = new Uint8Array(t.length); for (r = 0; r < t.length; r++) n[r] = t[r].toString().charCodeAt(0); var i, o, r = 0; for (r = 0; r < n.length; r++) 0 != (i = n[r] % 3) && r + i < n.length && (o = n[r + 1], n[r + 1] = n[r + i], n[r + i] = o, r = r + i + 1); return n } function i(t) { var e = ''; if ('object' == typeof t) for (var r = 0; r < t.length; r++) e += String.fromCharCode(t[r]); t = e || t; var n = new Uint8Array(t.length); for (r = 0; r < t.length; r++) n[r] = t[r].toString().charCodeAt(0); var r = 0 , i = 0 , o = 0 , a = 0; for (r = 0; r < n.length; r++) o = n[r] % 2, o && r++, a++; var s = new Uint8Array(a); for (r = 0; r < n.length; r++) o = n[r] % 2, s[i++] = o ? n[r++] : n[r]; return s } function o(t, e) { var r = 0 , n = 0 , i = 0 , o = 0 , a = ''; if ('object' == typeof t) for (var r = 0; r < t.length; r++) a += String.fromCharCode(t[r]); t = a || t; var s = new Uint8Array(t.length); for (r = 0; r < t.length; r++) s[r] = t[r].toString().charCodeAt(0); for (r = 0; r < t.length; r++) if (0 != (o = s[r] % 5) && 1 != o && r + o < s.length && (i = s[r + 1], n = r + 2, s[r + 1] = s[r + o], s[o + r] = i, (r = r + o + 1) - 2 > n)) for (; n < r - 2; n++) s[n] = s[n] ^ e.charCodeAt(n % e.length); for (r = 0; r < t.length; r++) s[r] = s[r] ^ e.charCodeAt(r % e.length); return s } for (var a = { data: { info: t } }, s = { q: r, h: n, m: i, k: o }, l = a.data.info, u = l.substring(l.length - 4).split(''), c = 0; c < u.length; c++) u[c] = u[c].toString().charCodeAt(0) % 4; u.reverse(); for (var d = [], c = 0; c < u.length; c++) d.push(l.substring(u[c] + 1, u[c] + 2)), l = l.substring(0, u[c] + 1) + l.substring(u[c] + 2); a.data.encrypt_table = d, a.data.key_table = []; for (var c in a.data.encrypt_table) 'q' != a.data.encrypt_table[c] && 'k' != a.data.encrypt_table[c] || (a.data.key_table.push(l.substring(l.length - 12)), l = l.substring(0, l.length - 12)); a.data.key_table.reverse(), a.data.info = l; var f = new Array(-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1); a.data.info = function(t) { var e, r, n, i, o, a, s; for (a = t.length, o = 0, s = ''; o < a; ) { do { e = f[255 & t.charCodeAt(o++)] } while (o < a && -1 == e);if (-1 == e) break; do { r = f[255 & t.charCodeAt(o++)] } while (o < a && -1 == r);if (-1 == r) break; s += String.fromCharCode(e << 2 | (48 & r) >> 4); do { if (61 == (n = 255 & t.charCodeAt(o++))) return s; n = f[n] } while (o < a && -1 == n);if (-1 == n) break; s += String.fromCharCode((15 & r) << 4 | (60 & n) >> 2); do { if (61 == (i = 255 & t.charCodeAt(o++))) return s; i = f[i] } while (o < a && -1 == i);if (-1 == i) break; s += String.fromCharCode((3 & n) << 6 | i) } return s }(a.data.info); for (var c in a.data.encrypt_table) { var h = a.data.encrypt_table[c]; if ('q' == h || 'k' == h) { var p = a.data.key_table.pop(); a.data.info = s[a.data.encrypt_table[c]](a.data.info, p) } else a.data.info = s[a.data.encrypt_table[c]](a.data.info) } if (e) return a.data.info; var g = ''; for (c = 0; c < a.data.info.length; c++) g += String.fromCharCode(a.data.info[c]); return g } function Uint8ArrayToString(fileData){ var dataString = ''; for (var i = 0; i < fileData.length; i++) { dataString += Number(fileData[i]) + ','; } return dataString; } function decodeKey(resp){ var string = eval('('+resp+')'); //return btoa(String.fromCharCode.apply(null, new Uint8Array(n(string.data.info, 1)))); return Uint8ArrayToString(new Uint8Array(n(string.data.info, 1))); } function decodeM3u8(resp){ var string = eval('('+resp+')'); return n(string.data.info); } "; public static string DecodeM3u8(string resp) { var context = new Context(); context.Eval(JS); var concatFunction = context.GetVariable("decodeM3u8").As(); string m3u8 = concatFunction.Call(new Arguments { resp }).ToString(); return m3u8; } public static string DecodeKey(string resp) { var context = new Context(); context.Eval(JS); var concatFunction = context.GetVariable("decodeKey").As(); string key = concatFunction.Call(new Arguments { resp }).ToString(); byte[] v = Array.ConvertAll(key.Trim(',').Split(','), s => (byte)int.Parse(s)); string realKey = Convert.ToBase64String(v); return realKey; } } } ================================================ FILE: N_m3u8DL-CLI/DecodeNfmovies.cs ================================================ using System; using System.IO; using System.Linq; using System.Text; namespace N_m3u8DL_CLI { class DecodeNfmovies { //https://jx.nfmovies.com/hls.min.js public static string DecryptM3u8(byte[] byteArray) { var t = byteArray; var decrypt = ""; if (137 == t[0] && 80 == t[1] && 130 == t[354] && 96 == t[353]) t = t.Skip(355).ToArray(); else { if (137 != t[0] || 80 != t[1] || 130 != t[394] || 96 != t[393]) { for (var i = 0; i < t.Length; i++) decrypt += Convert.ToChar(t[i]); return decrypt; } t = t.Skip(395).ToArray(); } using (var zipStream = new System.IO.Compression.GZipStream(new MemoryStream(t), System.IO.Compression.CompressionMode.Decompress)) { using (StreamReader sr = new StreamReader(zipStream, Encoding.UTF8)) { decrypt = sr.ReadToEnd(); } } return decrypt; } } } ================================================ FILE: N_m3u8DL-CLI/Decrypter.cs ================================================ using CSChaCha20; using System; using System.IO; using System.Linq; using System.Security.Cryptography; namespace N_m3u8DL_CLI { class Decrypter { public static byte[] AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) { FileStream fs = new FileStream(filePath, FileMode.Open); //获取文件大小 long size = fs.Length; byte[] inBuff = new byte[size]; fs.Read(inBuff, 0, inBuff.Length); fs.Close(); Aes dcpt = Aes.Create(); dcpt.BlockSize = 128; dcpt.KeySize = 128; dcpt.Key = keyByte; dcpt.IV = ivByte; dcpt.Mode = mode; dcpt.Padding = padding; ICryptoTransform cTransform = dcpt.CreateDecryptor(); Byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); return resultArray; } public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) { byte[] inBuff = encryptedBuff; Aes dcpt = Aes.Create(); dcpt.BlockSize = 128; dcpt.KeySize = 128; dcpt.Key = keyByte; dcpt.IV = ivByte; dcpt.Mode = mode; dcpt.Padding = padding; ICryptoTransform cTransform = dcpt.CreateDecryptor(); Byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); return resultArray; } public static byte[] CHACHA20Decrypt(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes) { if (keyBytes.Length != 32) throw new Exception("Key must be 32 bytes!"); if (nonceBytes.Length != 12 && nonceBytes.Length != 8) throw new Exception("Key must be 12 or 8 bytes!"); if (nonceBytes.Length == 8) nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray(); var decStream = new MemoryStream(); using (BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff))) { using (BinaryWriter writer = new BinaryWriter(decStream)) { while (true) { var buffer = reader.ReadBytes(1024); byte[] dec = new byte[buffer.Length]; if (buffer.Length > 0) { ChaCha20 forDecrypting = new ChaCha20(keyBytes, nonceBytes, 0); forDecrypting.DecryptBytes(dec, buffer); writer.Write(dec, 0, dec.Length); } else { break; } } } } return decStream.ToArray(); } public static byte[] HexStringToBytes(string hexStr) { if (string.IsNullOrEmpty(hexStr)) { return new byte[0]; } if (hexStr.StartsWith("0x") || hexStr.StartsWith("0X")) { hexStr = hexStr.Remove(0, 2); } int count = hexStr.Length; if (count % 2 == 1) { throw new ArgumentException("Invalid length of bytes:" + count); } int byteCount = count / 2; byte[] result = new byte[byteCount]; for (int ii = 0; ii < byteCount; ++ii) { var tempBytes = Byte.Parse(hexStr.Substring(2 * ii, 2), System.Globalization.NumberStyles.HexNumber); result[ii] = tempBytes; } return result; } } } ================================================ FILE: N_m3u8DL-CLI/DownloadManager.cs ================================================ using Newtonsoft.Json.Linq; using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace N_m3u8DL_CLI { class DownloadManager { private int stopCount = 0; //速度为零的停止 private string jsonFile = string.Empty; private int total = 0; public static string partsPadZero = string.Empty; string segsPadZero = string.Empty; private bool isVTT = false; bool externalAudio = false; //额外的音轨 string externalAudioUrl = ""; bool externalSub = false; //额外的字幕 string externalSubUrl = ""; string fflogName = "_ffreport.log"; public static bool BinaryMerge = false; public int Threads { get; set; } = 1; public int RetryCount { get; set; } = 5; public string Headers { get; set; } = string.Empty; public string DownDir { get; set; } = string.Empty; public string DownName { get; set; } = string.Empty; public bool DelAfterDone { get; set; } = false; public string MuxFormat { get; set; } = "mp4"; public bool MuxFastStart { get; set; } = true; public string MuxSetJson { get; set; } = string.Empty; public int TimeOut { get; set; } = 10000; //超时设置 public static double DownloadedSize { get; set; } = 0; //已下载大小 public static double ToDoSize { get; set; } = 0; //待下载大小 public static bool HasSetDir { get; set; } = false; public bool NoMerge { get; set; } = false; public static int CalcTime { get; set; } = 1; //计算速度的间隔 public static int Count { get; set; } = 0; public static int PartsCount { get; set; } = 0; public static bool DisableIntegrityCheck { get; set; } = false; //关闭完整性检查 public static bool HasExtMap { get; set; } = false; //是否有MAP static CancellationTokenSource cts = new CancellationTokenSource(); //计算下载速度 static System.Timers.Timer timer = new System.Timers.Timer(1000 * CalcTime); //实例化Timer类 public DownloadManager() { timer.AutoReset = true; timer.Elapsed += delegate { var eta = ""; if (ToDoSize != 0) { eta = " @ " + Global.FormatTime(Convert.ToInt32(ToDoSize / (Global.BYTEDOWN / CalcTime))); } var print = Global.FormatFileSize((Global.BYTEDOWN) / CalcTime) + "/s" + eta; ProgressReporter.Report("", "(" + print + ")"); if (Global.HadReadInfo && Global.BYTEDOWN <= Global.STOP_SPEED * 1024 * CalcTime) { stopCount++; eta = ""; if (ToDoSize != 0) { eta = " @ " + Global.FormatTime(Convert.ToInt32(ToDoSize / (Global.BYTEDOWN / CalcTime))); } print = Global.FormatFileSize((Global.BYTEDOWN) / CalcTime) + "/s [" + stopCount + "]" + eta; ProgressReporter.Report("", "(" + print + ")"); if (stopCount >= 12) { Global.ShouldStop = true; cts.Cancel(); timer.Enabled = false; } } else { stopCount = 0; Global.BYTEDOWN = 0; Global.ShouldStop = false; } }; } public void DoDownload() { jsonFile = Path.Combine(DownDir, "meta.json"); if (!File.Exists(jsonFile)) return; string jsonContent = File.ReadAllText(jsonFile); JObject initJson = JObject.Parse(jsonContent); JArray parts = JArray.Parse(initJson["m3u8Info"]["segments"].ToString()); //大分组 string segCount = initJson["m3u8Info"]["count"].ToString(); string oriCount = initJson["m3u8Info"]["originalCount"].ToString(); //原始分片数量 string isVOD = initJson["m3u8Info"]["vod"].ToString(); try { if (initJson["m3u8Info"]["audio"].ToString() != "") externalAudio = true; externalAudioUrl = initJson["m3u8Info"]["audio"].ToString(); LOGGER.WriteLine(strings.hasExternalAudioTrack); LOGGER.PrintLine(strings.hasExternalAudioTrack, LOGGER.Warning); } catch (Exception) {} try { if (initJson["m3u8Info"]["sub"].ToString() != "") externalSub = true; externalSubUrl = initJson["m3u8Info"]["sub"].ToString(); LOGGER.WriteLine(strings.hasExternalSubtitleTrack); LOGGER.PrintLine(strings.hasExternalSubtitleTrack, LOGGER.Warning); } catch (Exception) { } total = Convert.ToInt32(segCount); PartsCount = parts.Count; segsPadZero = string.Empty.PadRight(oriCount.Length, '0'); partsPadZero = string.Empty.PadRight(Convert.ToString(parts.Count).Length, '0'); //是直播视频 if (isVOD == "False") { return; } Global.ShouldStop = false; //是否该停止下载 if (!Directory.Exists(DownDir)) Directory.CreateDirectory(DownDir); //新建文件夹 Watcher watcher = new Watcher(DownDir); watcher.Total = total; watcher.PartsCount = PartsCount; watcher.WatcherStrat(); //开始计算速度 timer.Enabled = true; cts = new CancellationTokenSource(); //开始调用下载 LOGGER.WriteLine(strings.startDownloading); LOGGER.PrintLine(strings.startDownloading, LOGGER.Warning); //下载MAP文件(若有) downloadMap: if (HasExtMap) { LOGGER.PrintLine(strings.downloadingMapFile); Downloader sd = new Downloader(); sd.TimeOut = TimeOut; sd.FileUrl = initJson["m3u8Info"]["extMAP"].Value(); sd.Headers = Headers; sd.Method = "NONE"; if (sd.FileUrl.Contains("|")) //有range { string[] tmp = sd.FileUrl.Split('|'); sd.FileUrl = tmp[0]; sd.StartByte = Convert.ToUInt32(tmp[1].Split('@')[1]); sd.ExpectByte = Convert.ToUInt32(tmp[1].Split('@')[0]); } sd.SavePath = DownDir + "\\!MAP.tsdownloading"; if (File.Exists(sd.SavePath)) File.Delete(sd.SavePath); if (File.Exists(DownDir + "\\Part_0\\!MAP.ts")) File.Delete(DownDir + "\\Part_0\\!MAP.ts"); sd.Down(); //开始下载 if (!File.Exists(DownDir + "\\!MAP.ts")) //检测是否成功下载 { Thread.Sleep(1000); goto downloadMap; } } //首先下载第一个分片 JToken firstSeg = JArray.Parse(parts[0].ToString())[0]; if (!File.Exists(DownDir + "\\Part_" + 0.ToString(partsPadZero) + "\\" + firstSeg["index"].Value().ToString(segsPadZero) + ".ts")) { try { Downloader sd = new Downloader(); sd.TimeOut = TimeOut; sd.SegDur = firstSeg["duration"].Value(); if (sd.SegDur < 0) sd.SegDur = 0; //防止负数 sd.FileUrl = firstSeg["segUri"].Value(); //VTT字幕 if (isVTT == false && (sd.FileUrl.Trim('\"').EndsWith(".vtt") || sd.FileUrl.Trim('\"').EndsWith(".webvtt"))) isVTT = true; sd.Method = firstSeg["method"].Value(); if (sd.Method != "NONE") { sd.Key = firstSeg["key"].Value(); sd.Iv = firstSeg["iv"].Value(); } if (firstSeg["expectByte"] != null) sd.ExpectByte = firstSeg["expectByte"].Value(); if (firstSeg["startByte"] != null) sd.StartByte = firstSeg["startByte"].Value(); sd.Headers = Headers; sd.SavePath = DownDir + "\\Part_" + 0.ToString(partsPadZero) + "\\" + firstSeg["index"].Value().ToString(segsPadZero) + ".tsdownloading"; if (File.Exists(sd.SavePath)) File.Delete(sd.SavePath); LOGGER.PrintLine(strings.downloadingFirstSegement); if (!Global.ShouldStop) sd.Down(); //开始下载 } catch (Exception e) { //LOG.WriteLineError(e.ToString()); } } if (Global.HadReadInfo == false) { string href = DownDir + "\\Part_" + 0.ToString(partsPadZero) + "\\" + firstSeg["index"].Value().ToString(segsPadZero) + ".ts"; if (File.Exists(DownDir + "\\!MAP.ts")) href = DownDir + "\\!MAP.ts"; Global.GzipHandler(href); bool flag = false; foreach (string ss in (string[])Global.GetVideoInfo(href).ToArray(typeof(string))) { LOGGER.WriteLine(ss.Trim()); LOGGER.PrintLine(ss.Trim(), 0); if (ss.Trim().Contains("Error in reading file")) flag = true; } LOGGER.PrintLine(strings.waitForCompletion, LOGGER.Warning); if (!flag) Global.HadReadInfo = true; } //多线程设置 ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Threads, CancellationToken = cts.Token }; //构造包含所有分片的新的segments JArray segments = new JArray(); for (int i = 0; i < parts.Count; i++) { var tmp = JArray.Parse(parts[i].ToString()); for (int j = 0; j < tmp.Count; j++) { JObject t = (JObject)tmp[j]; t.Add("part", i); segments.Add(t); } } //剔除第一个分片(已下载过) segments.RemoveAt(0); try { ParallelLoopResult result = Parallel.ForEach(segments, parallelOptions, () => new Downloader(), (info, loopstate, index, sd) => { if (Global.ShouldStop) loopstate.Stop(); else { sd.TimeOut = TimeOut; sd.SegDur = info["duration"].Value(); if (sd.SegDur < 0) sd.SegDur = 0; //防止负数 sd.FileUrl = info["segUri"].Value(); //VTT字幕 if (isVTT == false && (sd.FileUrl.Trim('\"').EndsWith(".vtt") || sd.FileUrl.Trim('\"').EndsWith(".webvtt"))) isVTT = true; sd.Method = info["method"].Value(); if (sd.Method != "NONE") { sd.Key = info["key"].Value(); sd.Iv = info["iv"].Value(); } if (firstSeg["expectByte"] != null) sd.ExpectByte = info["expectByte"].Value(); if (firstSeg["startByte"] != null) sd.StartByte = info["startByte"].Value(); sd.Headers = Headers; sd.SavePath = DownDir + "\\Part_" + info["part"].Value().ToString(partsPadZero) + "\\" + info["index"].Value().ToString(segsPadZero) + ".tsdownloading"; if (File.Exists(sd.SavePath)) File.Delete(sd.SavePath); if (!Global.ShouldStop) sd.Down(); //开始下载 } return sd; }, (sd) => { }); if (result.IsCompleted) { //LOGGER.WriteLine("Part " + (info["part"].Value() + 1).ToString(partsPadZero) + " of " + parts.Count + " Completed"); } } catch (Exception) { ;//捕获取消循环产生的异常 } finally { cts.Dispose(); } watcher.WatcherStop(); //停止速度监测 timer.Enabled = false; //检测是否下完 IsComplete(Convert.ToInt32(segCount)); } public void IsComplete(int segCount) { int tsCount = 0; if (DisableIntegrityCheck) { tsCount = segCount; goto ll; } for (int i = 0; i < PartsCount; i++) { tsCount += Global.GetFileCount(DownDir + "\\Part_" + i.ToString(partsPadZero), ".ts"); } ll: if (tsCount != segCount) { LOGGER.PrintLine(strings.downloadedCount + tsCount + " / " + segCount); LOGGER.WriteLine(strings.downloadedCount + tsCount + " of " + segCount); if (Count <= RetryCount) { Count++; LOGGER.WriteLine(strings.retryCount + Count + " / " + RetryCount); LOGGER.PrintLine(strings.retryCount + Count + " / " + RetryCount, LOGGER.Warning); Thread.Sleep(3000); DoDownload(); } } else //开始合并 { LOGGER.PrintLine(strings.downloadComplete + (DisableIntegrityCheck ? "(" + strings.disableIntegrityCheck + ")" : "")); if (NoMerge == false) { string exePath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); string driverName = exePath.Remove(exePath.IndexOf(':')); Console.Title = "Done."; LOGGER.WriteLine(strings.startMerging); LOGGER.PrintLine(strings.startMerging, LOGGER.Warning); //VTT字幕 if (isVTT == true) { MuxFormat = "vtt"; Global.ReAdjustVtt(Global.GetFiles(DownDir + "\\Part_0", ".ts")); } //只有一个Part直接用ffmpeg合并 if (PartsCount == 1) { /* * FFREPORT=file=C\:/Users/nilao/Desktop/新建文件夹/3.log:level=32 * Test with Powershell, its C:/Users/nilao/Desktop/新建文件夹/3.log */ FFmpeg.OutPutPath = Path.Combine(Directory.GetParent(DownDir).FullName, DownName); FFmpeg.ReportFile = driverName + "\\:" + exePath.Remove(0, exePath.IndexOf(':') + 1).Replace("\\", "/") + "/Logs/" + Path.GetFileNameWithoutExtension(LOGGER.LOGFILE) + fflogName; if (File.Exists(DownDir + "\\!MAP.ts")) File.Move(DownDir + "\\!MAP.ts", DownDir + "\\Part_0\\!MAP.ts"); if (BinaryMerge) { LOGGER.PrintLine(strings.binaryMergingPleaseWait); MuxFormat = "ts"; //有MAP文件,一般为mp4,采取默认动作 if(File.Exists(DownDir + "\\Part_0\\!MAP.ts")) MuxFormat = "mp4"; if (isVTT) MuxFormat = "vtt"; if (Global.AUDIO_TYPE != "") MuxFormat = Global.AUDIO_TYPE; Global.CombineMultipleFilesIntoSingleFile(Global.GetFiles(DownDir + "\\Part_0", ".ts"), FFmpeg.OutPutPath + $".{MuxFormat}"); } else { if (Global.VIDEO_TYPE != "DV") //不是杜比视界 { //检测是否为MPEG-TS封装,不是的话就转换为TS封装 foreach (string s in Global.GetFiles(DownDir + "\\Part_0", ".ts")) { //跳过有MAP的情况 if (!isVTT && !File.Exists(DownDir + "\\Part_0\\!MAP.ts") && !FFmpeg.CheckMPEGTS(s)) { //转换 LOGGER.PrintLine(strings.remuxToMPEGTS + Path.GetFileName(s)); LOGGER.WriteLine(strings.remuxToMPEGTS + Path.GetFileName(s)); FFmpeg.ConvertToMPEGTS(s); } } //分片过多的情况 if (tsCount >= 1800) { LOGGER.WriteLine(strings.partialMergingPleaseWait); LOGGER.PrintLine(strings.partialMergingPleaseWait, LOGGER.Warning); Global.PartialCombineMultipleFiles(Global.GetFiles(DownDir + "\\Part_0", ".ts")); } if (Global.AUDIO_TYPE != "") MuxFormat = Global.AUDIO_TYPE; LOGGER.PrintLine(strings.ffmpegMergingPleaseWait); if (!File.Exists(MuxSetJson)) FFmpeg.Merge(Global.GetFiles(DownDir + "\\Part_0", ".ts"), MuxFormat, MuxFastStart); else { JObject json = JObject.Parse(File.ReadAllText(MuxSetJson, Encoding.UTF8)); string muxFormat = json["muxFormat"].Value(); bool fastStart = Convert.ToBoolean(json["fastStart"].Value()); string poster = json["poster"].Value(); string audioName = json["audioName"].Value(); string title = json["title"].Value(); string copyright = json["copyright"].Value(); string comment = json["comment"].Value(); string encodingTool = ""; try { encodingTool = json["encodingTool"].Value(); } catch (Exception) {; } FFmpeg.Merge(Global.GetFiles(DownDir + "\\Part_0", ".ts"), muxFormat, fastStart, poster, audioName, title, copyright, comment, encodingTool); } //Global.CombineMultipleFilesIntoSingleFile(Global.GetFiles(DownDir + "\\Part_0", ".ts"), FFmpeg.OutPutPath + ".ts"); //Global.ExplorerFile(FFmpeg.OutPutPath + ".mp4"); } else { LOGGER.PrintLine(strings.dolbyVisionContentMerging); Global.CombineMultipleFilesIntoSingleFile(Global.GetFiles(DownDir + "\\Part_0", ".ts"), FFmpeg.OutPutPath + ".mp4"); } } LOGGER.WriteLine(strings.taskDone + "\r\n\r\nTask End: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + "\r\nFile: " + FFmpeg.OutPutPath + "." + (MuxFormat == "aac" ? "m4a" : MuxFormat) + "\r\n\r\n"); //删除文件夹 if (DelAfterDone) { try { DirectoryInfo directoryInfo = new DirectoryInfo(DownDir); directoryInfo.Delete(true); } catch (Exception) { } } if (externalAudio) //下载独立音轨 { externalAudio = false; DownloadedSize = 0; Global.WriteInit(); LOGGER.PrintLine(strings.downloadingExternalAudioTrack, LOGGER.Warning); Parser parser = new Parser(); parser.Headers = Headers; //继承Header parser.BaseUrl = ""; parser.M3u8Url = externalAudioUrl; parser.DownName = DownName + "(Audio)"; parser.DownDir = Path.Combine(Path.GetDirectoryName(DownDir), parser.DownName); LOGGER.WriteLine(strings.startParsing + externalAudioUrl); LOGGER.WriteLine(strings.downloadingExternalAudioTrack); DownName = DownName + "(Audio)"; fflogName = "_ffreport(Audio).log"; DownDir = parser.DownDir; parser.Parse(); //开始解析 Thread.Sleep(1000); Global.HadReadInfo = false; Global.VIDEO_TYPE = ""; Global.AUDIO_TYPE = ""; DoDownload(); } if (externalSub) //下载独立字幕 { externalSub = false; DownloadedSize = 0; Global.WriteInit(); LOGGER.PrintLine(strings.downloadingExternalSubtitleTrack, LOGGER.Warning); Parser parser = new Parser(); parser.Headers = Headers; //继承Header parser.BaseUrl = ""; parser.M3u8Url = externalSubUrl; parser.DownName = DownName.Replace("(Audio)", "") + "(Subtitle)"; parser.DownDir = Path.Combine(Path.GetDirectoryName(DownDir), parser.DownName); LOGGER.WriteLine(strings.startParsing + externalSubUrl); LOGGER.WriteLine(strings.downloadingExternalSubtitleTrack); DownName = parser.DownName; fflogName = "_ffreport(Subtitle).log"; DownDir = parser.DownDir; parser.Parse(); //开始解析 Thread.Sleep(1000); Global.HadReadInfo = false; Global.VIDEO_TYPE = ""; Global.AUDIO_TYPE = ""; DoDownload(); } LOGGER.PrintLine(strings.taskDone, LOGGER.Warning); Environment.Exit(0); //正常退出程序 return; } FFmpeg.OutPutPath = Path.Combine(Directory.GetParent(DownDir).FullName, DownName); FFmpeg.ReportFile = driverName + "\\:" + exePath.Remove(0, exePath.IndexOf(':') + 1).Replace("\\", "/") + "/Logs/" + Path.GetFileNameWithoutExtension(LOGGER.LOGFILE) + fflogName; //合并分段 LOGGER.PrintLine(strings.startMerging); for (int i = 0; i < PartsCount; i++) { string outputFilePath = DownDir + "\\Part_" + i.ToString(partsPadZero) + ".ts"; Global.CombineMultipleFilesIntoSingleFile( Global.GetFiles(DownDir + "\\Part_" + i.ToString(partsPadZero), ".ts"), outputFilePath); try { DirectoryInfo directoryInfo = new DirectoryInfo(DownDir + "\\Part_" + i.ToString(partsPadZero)); directoryInfo.Delete(true); } catch (Exception) { } } if (BinaryMerge) { LOGGER.PrintLine(strings.binaryMergingPleaseWait); MuxFormat = "ts"; //有MAP文件,一般为mp4,采取默认动作 if (File.Exists(DownDir + "\\!MAP.ts")) MuxFormat = "mp4"; if (isVTT) MuxFormat = "vtt"; Global.CombineMultipleFilesIntoSingleFile(Global.GetFiles(DownDir, ".ts"), FFmpeg.OutPutPath + $".{MuxFormat}"); } else { if (Global.VIDEO_TYPE != "DV") //不是爱奇艺杜比视界 { //检测是否为MPEG-TS封装,不是的话就转换为TS封装 foreach (string s in Global.GetFiles(DownDir, ".ts")) { //跳过有MAP的情况 if (!isVTT && !File.Exists(DownDir + "\\!MAP.ts") && !FFmpeg.CheckMPEGTS(s)) { //转换 LOGGER.PrintLine(strings.remuxToMPEGTS + Path.GetFileName(s)); LOGGER.WriteLine(strings.remuxToMPEGTS + Path.GetFileName(s)); FFmpeg.ConvertToMPEGTS(s); } } if (Global.AUDIO_TYPE != "") MuxFormat = Global.AUDIO_TYPE; LOGGER.PrintLine(strings.ffmpegMergingPleaseWait); if (!File.Exists(MuxSetJson)) FFmpeg.Merge(Global.GetFiles(DownDir, ".ts"), MuxFormat, MuxFastStart); else { JObject json = JObject.Parse(File.ReadAllText(MuxSetJson, Encoding.UTF8)); string muxFormat = json["muxFormat"].Value(); bool fastStart = Convert.ToBoolean(json["fastStart"].Value()); string poster = json["poster"].Value(); string audioName = json["audioName"].Value(); string title = json["title"].Value(); string copyright = json["copyright"].Value(); string comment = json["comment"].Value(); string encodingTool = ""; try { encodingTool = json["encodingTool"].Value(); } catch (Exception) {; } FFmpeg.Merge(Global.GetFiles(DownDir, ".ts"), muxFormat, fastStart, poster, audioName, title, copyright, comment, encodingTool); } } else { LOGGER.PrintLine(strings.dolbyVisionContentMerging); Global.CombineMultipleFilesIntoSingleFile(Global.GetFiles(DownDir, ".ts"), FFmpeg.OutPutPath + ".mp4"); } } LOGGER.WriteLine(strings.taskDone + "\r\n\r\nTask End: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + "\r\nFile: " + FFmpeg.OutPutPath + "." + (MuxFormat == "aac" ? "m4a" : MuxFormat) + "\r\n\r\n"); //Global.ExplorerFile(FFmpeg.OutPutPath + ".mp4"); //删除文件夹 if (DelAfterDone) { try { DirectoryInfo directoryInfo = new DirectoryInfo(DownDir); directoryInfo.Delete(true); } catch (Exception) { } } if (externalAudio) //下载独立音轨 { externalAudio = false; DownloadedSize = 0; Global.WriteInit(); LOGGER.PrintLine(strings.downloadingExternalAudioTrack, LOGGER.Warning); Parser parser = new Parser(); parser.Headers = Headers; //继承Header parser.BaseUrl = ""; parser.M3u8Url = externalAudioUrl; parser.DownName = DownName + "(Audio)"; parser.DownDir = Path.Combine(Path.GetDirectoryName(DownDir), parser.DownName); LOGGER.WriteLine(strings.startParsing + externalAudioUrl); LOGGER.WriteLine(strings.downloadingExternalAudioTrack); DownName = parser.DownName; fflogName = "_ffreport(Audio).log"; DownDir = parser.DownDir; parser.Parse(); //开始解析 Thread.Sleep(1000); Global.HadReadInfo = false; Global.VIDEO_TYPE = ""; Global.AUDIO_TYPE = ""; DoDownload(); } if (externalSub) //下载独立字幕 { externalSub = false; DownloadedSize = 0; Global.WriteInit(); LOGGER.PrintLine(strings.downloadingExternalSubtitleTrack, LOGGER.Warning); Parser parser = new Parser(); parser.Headers = Headers; //继承Header parser.BaseUrl = ""; parser.M3u8Url = externalSubUrl; parser.DownName = DownName.Replace("(Audio)", "") + "(Subtitle)"; parser.DownDir = Path.Combine(Path.GetDirectoryName(DownDir), parser.DownName); LOGGER.WriteLine(strings.startParsing + externalSubUrl); LOGGER.WriteLine(strings.downloadingExternalSubtitleTrack); DownName = parser.DownName; fflogName = "_ffreport(Subtitle).log"; DownDir = parser.DownDir; parser.Parse(); //开始解析 Thread.Sleep(1000); Global.HadReadInfo = false; Global.VIDEO_TYPE = ""; Global.AUDIO_TYPE = ""; DoDownload(); } LOGGER.PrintLine(strings.taskDone, LOGGER.Warning); Environment.Exit(0); //正常退出程序 } else { Console.Title = "Done."; LOGGER.PrintLine(strings.taskDone, LOGGER.Warning); LOGGER.WriteLine(strings.taskDone + "\r\n\r\nTask End: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")); Environment.Exit(0); //正常退出程序 } } } } } ================================================ FILE: N_m3u8DL-CLI/Downloader.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace N_m3u8DL_CLI { class Downloader { private int timeOut = 0; private int retry = 5; private int count = 0; private int segIndex = 0; private double segDur = 0; private string fileUrl = string.Empty; private string savePath = string.Empty; private string headers = string.Empty; private string method = string.Empty; private string key = string.Empty; private string iv = string.Empty; private string liveFile = string.Empty; private long expectByte = -1; private long startByte = 0; private bool isLive = false; private bool isDone = false; private bool firstSeg = true; private FileStream liveStream = null; public string FileUrl { get => fileUrl; set => fileUrl = value; } public string SavePath { get => savePath; set => savePath = value; } public string Headers { get => headers; set => headers = value; } public string Method { get => method; set => method = value; } public string Key { get => key; set => key = value; } public string Iv { get => iv; set => iv = value; } public bool IsLive { get => isLive; set => isLive = value; } public int Retry { get => retry; set => retry = value; } public bool IsDone { get => isDone; set => isDone = value; } public int SegIndex { get => segIndex; set => segIndex = value; } public int TimeOut { get => timeOut; set => timeOut = value; } public FileStream LiveStream { get => liveStream; set => liveStream = value; } public string LiveFile { get => liveFile; set => liveFile = value; } public long ExpectByte { get => expectByte; set => expectByte = value; } public long StartByte { get => startByte; set => startByte = value; } public double SegDur { get => segDur; set => segDur = value; } public static bool EnableChaCha20 { get; set; } = false; public static string ChaCha20KeyBase64 { get; set; } public static string ChaCha20NonceBase64 { get; set; } //重写WebClinet //private class WebClient : System.Net.WebClient //{ // protected override WebRequest GetWebRequest(Uri uri) // { // WebRequest lWebRequest = base.GetWebRequest(uri); // lWebRequest.Timeout = TimeOut; // ((HttpWebRequest)lWebRequest).ReadWriteTimeout = TimeOut; // return lWebRequest; // } //} //WebClient client = new WebClient(); public void Down() { try { //直播下载 if (IsLive) { IsDone = false; //设置为未完成下载 if (Method == "NONE" || method.Contains("NOTSUPPORTED")) { LOGGER.PrintLine("<" + SegIndex + " Downloading>"); LOGGER.WriteLine("<" + SegIndex + " Downloading>"); byte[] segBuff = Global.HttpDownloadFileToBytes(fileUrl, Headers, TimeOut); //byte[] segBuff = Global.WebClientDownloadToBytes(fileUrl, Headers); Global.AppendBytesToFileStreamAndDoNotClose(LiveStream, segBuff); LOGGER.PrintLine("<" + SegIndex + " Complete>\r\n"); LOGGER.WriteLine("<" + SegIndex + " Complete>"); IsDone = true; } else if (Method == "AES-128") { LOGGER.PrintLine("<" + SegIndex + " Downloading>"); LOGGER.WriteLine("<" + SegIndex + " Downloading>"); byte[] encryptedBuff = Global.HttpDownloadFileToBytes(fileUrl, Headers, TimeOut); //byte[] encryptedBuff = Global.WebClientDownloadToBytes(fileUrl, Headers); byte[] decryptBuff = null; decryptBuff = Decrypter.AES128Decrypt( encryptedBuff, Convert.FromBase64String(Key), Decrypter.HexStringToBytes(Iv) ); Global.AppendBytesToFileStreamAndDoNotClose(LiveStream, decryptBuff); LOGGER.PrintLine("<" + SegIndex + " Complete>\r\n"); LOGGER.WriteLine("<" + SegIndex + " Complete>"); IsDone = true; } else { //LOGGER.PrintLine("不支持这种加密方式!", LOGGER.Error); IsDone = true; } if (firstSeg && Global.FileSize(LiveFile) != 0) { //LOGGER.STOPLOG = false; //记录日志 foreach (string ss in (string[])Global.GetVideoInfo(LiveFile).ToArray(typeof(string))) { LOGGER.WriteLine(ss.Trim()); } firstSeg = false; //LOGGER.STOPLOG = true; //停止记录日志 } HLSLiveDownloader.REC_DUR += SegDur; if (HLSLiveDownloader.REC_DUR_LIMIT != -1 && HLSLiveDownloader.REC_DUR >= HLSLiveDownloader.REC_DUR_LIMIT) { LOGGER.PrintLine(strings.recordLimitReached, LOGGER.Warning); LOGGER.WriteLine(strings.recordLimitReached); Environment.Exit(0); //正常退出 } return; } //点播下载 else { if (!Directory.Exists(Path.GetDirectoryName(SavePath))) Directory.CreateDirectory(Path.GetDirectoryName(SavePath)); //新建文件夹 //是否存在文件,存在则不下载 if (File.Exists(Path.GetDirectoryName(savePath) + "\\" + Path.GetFileNameWithoutExtension(savePath) + ".ts")) { Global.BYTEDOWN++; //防止被速度监控程序杀死 //Console.WriteLine("Exists " + Path.GetFileNameWithoutExtension(savePath) + ".ts"); return; } //Console.WriteLine("开始下载 " + fileUrl); //本地文件 if (fileUrl.StartsWith("file:")) { Uri t = new Uri(fileUrl); fileUrl = t.LocalPath; if (File.Exists(fileUrl)) { if (ExpectByte == -1) //没有RANGE { FileInfo fi = new FileInfo(fileUrl); fi.CopyTo(savePath); Global.BYTEDOWN += fi.Length; } else { FileStream stream = new FileInfo(fileUrl).OpenRead(); //seek文件 stream.Seek(StartByte, SeekOrigin.Begin); Byte[] buffer = new Byte[ExpectByte]; //从流中读取字节块并将该数据写入给定缓冲区buffer中 stream.Read(buffer, 0, Convert.ToInt32(buffer.Length)); stream.Close(); //写出文件 MemoryStream m = new MemoryStream(buffer); FileStream fs = new FileStream(savePath, FileMode.OpenOrCreate); m.WriteTo(fs); m.Close(); fs.Close(); m = null; fs = null; } } } else { //下载 Global.HttpDownloadFile(fileUrl, savePath, TimeOut, Headers, StartByte, ExpectByte); } } if (File.Exists(savePath) && Global.ShouldStop == false) { FileInfo fi = new FileInfo(savePath); if (File.Exists(fi.FullName) && EnableChaCha20) { byte[] decryptBuff = Decrypter.CHACHA20Decrypt(File.ReadAllBytes(fi.FullName), Convert.FromBase64String(ChaCha20KeyBase64), Convert.FromBase64String(ChaCha20NonceBase64)); FileStream fs = new FileStream(Path.GetDirectoryName(SavePath) + "\\" + Path.GetFileNameWithoutExtension(SavePath) + ".ts", FileMode.Create); fs.Write(decryptBuff, 0, decryptBuff.Length); fs.Close(); DownloadManager.DownloadedSize += fi.Length; fi.Delete(); } else if (Method == "NONE" || Method.Contains("NOTSUPPORTED")) { fi.MoveTo(Path.GetDirectoryName(SavePath) + "\\" + Path.GetFileNameWithoutExtension(SavePath) + ".ts"); DownloadManager.DownloadedSize += fi.Length; //Console.WriteLine(Path.GetFileNameWithoutExtension(savePath) + " Completed."); } else if (File.Exists(fi.FullName) && Method == "AES-128") { //解密 try { byte[] decryptBuff = null; if(fileUrl.Contains(".51cto.com/")) //使用AES-128-ECB模式解密 { decryptBuff = Decrypter.AES128Decrypt( fi.FullName, Convert.FromBase64String(Key), Decrypter.HexStringToBytes(Iv), System.Security.Cryptography.CipherMode.ECB ); } else { decryptBuff = Decrypter.AES128Decrypt( fi.FullName, Convert.FromBase64String(Key), Decrypter.HexStringToBytes(Iv) ); } FileStream fs = new FileStream(Path.GetDirectoryName(savePath) + "\\" + Path.GetFileNameWithoutExtension(savePath) + ".ts", FileMode.Create); fs.Write(decryptBuff, 0, decryptBuff.Length); fs.Close(); DownloadManager.DownloadedSize += fi.Length; fi.Delete(); //Console.WriteLine(Path.GetFileNameWithoutExtension(savePath) + " Completed & Decrypted."); } catch (Exception ex) { LOGGER.PrintLine(ex.Message, LOGGER.Error); LOGGER.WriteLineError(ex.Message); Thread.Sleep(3000); Environment.Exit(-1); } } else { LOGGER.WriteLineError(strings.SomethingWasWrong); LOGGER.PrintLine(strings.SomethingWasWrong, LOGGER.Error); return; } return; } } catch (Exception ex) { LOGGER.WriteLineError(ex.Message); if (ex.Message.Contains("404") || ex.Message.Contains("400"))//(400) 错误的请求,片段过期会提示400错误 { IsDone = true; return; } else if (IsLive && count++ < Retry) { Thread.Sleep(2000);//直播一般3-6秒一个片段 Down(); } } } } } ================================================ FILE: N_m3u8DL-CLI/FFmpeg.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace N_m3u8DL_CLI { class FFmpeg { public static string FFMPEG_PATH = "ffmpeg"; public static string REC_TIME = ""; //录制日期 public static string OutPutPath { get; set; } = string.Empty; public static string ReportFile { get; set; } = string.Empty; public static bool UseAACFilter { get; set; } = false; //是否启用滤镜 public static bool WriteDate { get; set; } = true; //是否写入录制日期 public static void Merge(string[] files, string muxFormat, bool fastStart, string poster = "", string audioName = "", string title = "", string copyright = "", string comment = "", string encodingTool = "") { string dateString = string.IsNullOrEmpty(REC_TIME) ? DateTime.Now.ToString("o") : REC_TIME; //同名文件已存在的共存策略 if (File.Exists($"{OutPutPath}.{muxFormat.ToLower()}")) { OutPutPath = Path.Combine(Path.GetDirectoryName(OutPutPath), Path.GetFileName(OutPutPath) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")); } string command = "-loglevel warning -i concat:\""; string data = string.Empty; string ddpAudio = string.Empty; string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic"; ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(OutPutPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(OutPutPath + ".mp4")}.txt") : "") ; if (!string.IsNullOrEmpty(ddpAudio)) UseAACFilter = false; foreach (string t in files) { command += Path.GetFileName(t) + "|"; } switch (muxFormat.ToUpper()) { case ("MP4"): command += "\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\""); command += " " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\""); command += $" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? "-map 0:a?" : $"-map {(string.IsNullOrEmpty(poster) ? "1" : "2")}:a -map 0:a?")} -map 0:s? " + (string.IsNullOrEmpty(poster) ? "" : addPoster) + (WriteDate ? " -metadata date=\"" + dateString + "\"" : "") + " -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title + "\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler_name=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" "; command += (string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 handler_name=\"DD+\" -metadata:s:a:0 handler=\"DD+\" "); if (fastStart) command += "-movflags +faststart"; command += " -c copy -y " + (UseAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + OutPutPath + ".mp4\""; break; case ("MKV"): command += "\" -map 0 -c copy -y " + (UseAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + OutPutPath + ".mkv\""; break; case ("FLV"): command += "\" -map 0 -c copy -y " + (UseAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + OutPutPath + ".flv\""; break; case ("TS"): command += "\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + OutPutPath + ".ts\""; break; case ("VTT"): command += "\" -map 0 -y \"" + OutPutPath + ".srt\""; //Convert To Srt break; case ("EAC3"): command += "\" -map 0:a -c copy -y \"" + OutPutPath + ".eac3\""; break; case ("AAC"): command += "\" -map 0:a -c copy -y \"" + OutPutPath + ".m4a\""; break; case ("AC3"): command += "\" -map 0:a -c copy -y \"" + OutPutPath + ".ac3\""; break; } Run(FFMPEG_PATH, command, Path.GetDirectoryName(files[0])); LOGGER.WriteLine(strings.ffmpegDone); //Console.WriteLine(command); } public static void ConvertToMPEGTS(string file) { if (Global.VIDEO_TYPE == "H264") { Run(FFMPEG_PATH, "-loglevel quiet -i \"" + file + "\" -map 0 -c copy -copy_unknown -f mpegts -bsf:v h264_mp4toannexb \"" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts\"", Path.GetDirectoryName(file)); if (File.Exists(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts")) { File.Delete(file); File.Move(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts", file); } } else if (Global.VIDEO_TYPE == "H265") { Run(FFMPEG_PATH, "-loglevel quiet -i \"" + file + "\" -map 0 -c copy -copy_unknown -f mpegts -bsf:v hevc_mp4toannexb \"" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts\"", Path.GetDirectoryName(file)); if (File.Exists(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts")) { File.Delete(file); File.Move(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts", file); } } else { LOGGER.WriteLineError("Unkown Video Type"); } } public static void Run(string path, string args, string workDir) { string nowDir = Directory.GetCurrentDirectory(); //当前工作路径 Directory.SetCurrentDirectory(workDir); Process p = new Process();//建立外部调用线程 p.StartInfo.FileName = path;//要调用外部程序的绝对路径 Environment.SetEnvironmentVariable("FFREPORT", "file=" + ReportFile + ":level=32"); //兼容XP系统 //p.StartInfo.Environment.Add("FFREPORT", "file=" + ReportFile + ":level=32"); p.StartInfo.Arguments = args;//参数(这里就是FFMPEG的参数了) p.StartInfo.UseShellExecute = false;//不使用操作系统外壳程序启动线程(一定为FALSE,详细的请看MSDN) p.StartInfo.RedirectStandardError = true;//把外部程序错误输出写到StandardError流中(这个一定要注意,FFMPEG的所有输出信息,都为错误输出流,用StandardOutput是捕获不到任何消息的...这是我耗费了2个多月得出来的经验...mencoder就是用standardOutput来捕获的) p.StartInfo.CreateNoWindow = false;//不创建进程窗口 p.ErrorDataReceived += new DataReceivedEventHandler(Output);//外部程序(这里是FFMPEG)输出流时候产生的事件,这里是把流的处理过程转移到下面的方法中,详细请查阅MSDN p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.Start();//启动线程 p.BeginErrorReadLine();//开始异步读取 p.WaitForExit();//阻塞等待进程结束 p.Close();//关闭进程 p.Dispose();//释放资源 Environment.SetEnvironmentVariable("FFREPORT", null); //兼容XP系统 Directory.SetCurrentDirectory(nowDir); } private static void Output(object sendProcess, DataReceivedEventArgs output) { if (!String.IsNullOrEmpty(output.Data)) { LOGGER.PrintLine(output.Data, LOGGER.Warning); } } public static bool CheckMPEGTS(string file) { //放行杜比视界或纯音频文件 if (Global.VIDEO_TYPE == "DV" || Global.AUDIO_TYPE != "") return true; //如果是多分片,也认为不是MPEGTS if (DownloadManager.PartsCount > 1) return false; using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read)) { byte[] firstByte = new byte[1]; fs.Read(firstByte, 0, 1); //第一字节的16进制字符串 string _1_byte_str = Convert.ToString(firstByte[0], 16); //syncword不为47就不处理 if (_1_byte_str != "47") return false; } return true; } } } ================================================ FILE: N_m3u8DL-CLI/Global.cs ================================================ using BrotliSharpLib; using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; namespace N_m3u8DL_CLI { class Global { private volatile static bool shouldStop = false; public static long BYTEDOWN = 0; public static long STOP_SPEED = 0; //KB 小于此值自动重试 public static long MAX_SPEED = 0; //KB 速度上限 public static string VIDEO_TYPE = ""; public static string AUDIO_TYPE = ""; public static bool HadReadInfo = false; private static bool noProxy = false; private static string useProxyAddress = ""; public static bool ShouldStop { get => shouldStop; set => shouldStop = value; } public static bool NoProxy { get => noProxy; set => noProxy = value; } public static string UseProxyAddress { get => useProxyAddress; set => useProxyAddress = value; } /*===============================================================================*/ static Version ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; static string nowVer = $"{ver.Major}.{ver.Minor}.{ver.Build}"; static string nowDate = "20220711"; public static void WriteInit() { Console.WriteLine($"N_m3u8DL-CLI version {nowVer} 2018-2022"); Console.WriteLine($" built date: {nowDate}"); Console.WriteLine(); } public static void CheckUpdate() { try { string redirctUrl = Get302("https://github.com/nilaoda/N_m3u8DL-CLI/releases/latest"); string latestVer = redirctUrl.Replace("https://github.com/nilaoda/N_m3u8DL-CLI/releases/tag/", ""); if (nowVer != latestVer && !latestVer.StartsWith("https")) { Console.Title = string.Format(strings.newerVisionDetected, latestVer); try { //尝试下载新版本 string url = $"https://mirror.ghproxy.com/https://github.com/nilaoda/N_m3u8DL-CLI/releases/download/{latestVer}/N_m3u8DL-CLI_v{latestVer}.exe"; if (File.Exists(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), $"N_m3u8DL-CLI_v{latestVer}.exe"))) { Console.Title = string.Format(strings.newerVerisonDownloaded, latestVer); return; } HttpDownloadFile(url, Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), $"N_m3u8DL-CLI_v{latestVer}.exe")); if (File.Exists(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), $"N_m3u8DL-CLI_v{latestVer}.exe"))) Console.Title = string.Format(strings.newerVerisonDownloaded, latestVer); else Console.Title = string.Format(strings.newerVerisonDownloadFailed, latestVer); } catch (Exception) { ; } } } catch (Exception) { ; } } public static string GetValidFileName(string input, string re = ".") { string title = input; foreach (char invalidChar in Path.GetInvalidFileNameChars()) { title = title.Replace(invalidChar.ToString(), re); } return title; } // parseInt(s, radix) public static int GetNum(string str, int numBase) { return Convert.ToInt32(Microsoft.JScript.GlobalObject.parseInt(str, numBase)); } // 统一设置代理 // 替换 else if (UseProxyAddress != "") { // WebProxy proxy = new WebProxy(UseProxyAddress); // webRequest.Proxy = proxy; // } public static void SetProxy(WebRequest webRequest) { var g_ProxyAddress = UseProxyAddress; if (g_ProxyAddress.StartsWith("http://")) { WebProxy proxy = new WebProxy(g_ProxyAddress); //proxy.Credentials = new NetworkCredential(username, password); webRequest.Proxy = proxy; } // socks5 if (g_ProxyAddress.StartsWith("socks5://")) { string input = g_ProxyAddress.Remove(0, 9); if (input.EndsWith("/")) { input = input.Remove(input.LastIndexOf('/'), 1); } string[] addr = input.Split(':'); //LOGGER.PrintLine("addr Length :" + addr.Length); if (addr.Length == 2) { int port = 0; if (int.TryParse(addr[1], out port)) { var proxySocks5 = new MihaZupan.HttpToSocks5Proxy(addr[0], int.Parse(addr[1])); webRequest.Proxy = proxySocks5; //LOGGER.PrintLine("sock5 :" + addr[0] + ":" + addr[1]); } } else { LOGGER.PrintLine("Socks5addr String Length : " + addr.Length); } } } //获取网页源码 public static string GetWebSource(String url, string headers = "", int TimeOut = 60000) { string htmlCode = string.Empty; for (int i = 0; i < 5; i++) { try { reProcess: HttpWebRequest webRequest = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); webRequest.Method = "GET"; if (NoProxy) { webRequest.Proxy = null; } else if (UseProxyAddress != "") { SetProxy(webRequest); } webRequest.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"; webRequest.Accept = "*/*"; webRequest.Headers.Add("Accept-Encoding", "gzip, deflate, br"); webRequest.Timeout = TimeOut; //设置超时 webRequest.KeepAlive = false; webRequest.AllowAutoRedirect = false; //手动处理重定向,否则会丢失Referer if (url.Contains("pcvideo") && url.Contains(".titan.mgtv.com")) { webRequest.UserAgent = ""; if (!url.Contains("/internettv/")) webRequest.Referer = "https://www.mgtv.com"; webRequest.Headers.Add("Cookie", "MQGUID"); } //添加headers if (headers != "") { foreach (string att in headers.Split('|')) { try { if (att.Split(':')[0].ToLower() == "referer") webRequest.Referer = att.Substring(att.IndexOf(":") + 1); else if (att.Split(':')[0].ToLower() == "user-agent") webRequest.UserAgent = att.Substring(att.IndexOf(":") + 1); else if (att.Split(':')[0].ToLower() == "range") webRequest.AddRange(Convert.ToInt32(att.Substring(att.IndexOf(":") + 1).Split('-')[0], Convert.ToInt32(att.Substring(att.IndexOf(":") + 1).Split('-')[1]))); else if (att.Split(':')[0].ToLower() == "accept") webRequest.Accept = att.Substring(att.IndexOf(":") + 1); else webRequest.Headers.Add(att); } catch (Exception e) { LOGGER.WriteLineError(e.Message); } } } HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse(); //302 if (webResponse.Headers.Get("Location") != null) { url = webResponse.Headers.Get("Location"); webResponse.Close(); goto reProcess; } //文件过大则认为不是m3u8 if (webResponse.ContentLength != -1 && webResponse.ContentLength > 50 * 1024 * 1024) return ""; if (webResponse.ContentEncoding != null && webResponse.ContentEncoding.ToLower() == "gzip") //如果使用了GZip则先解压 { using (Stream streamReceive = webResponse.GetResponseStream()) { using (var zipStream = new System.IO.Compression.GZipStream(streamReceive, System.IO.Compression.CompressionMode.Decompress)) { using (StreamReader sr = new StreamReader(zipStream, Encoding.UTF8)) { htmlCode = sr.ReadToEnd(); } } } } else if (webResponse.ContentEncoding != null && webResponse.ContentEncoding.ToLower() == "br") //如果使用了Brotli则先解压 { using (Stream streamReceive = webResponse.GetResponseStream()) { using (var bs = new BrotliStream(streamReceive, CompressionMode.Decompress)) { using (StreamReader sr = new StreamReader(bs, Encoding.UTF8)) { htmlCode = sr.ReadToEnd(); } } } } else { using (Stream streamReceive = webResponse.GetResponseStream()) { using (StreamReader sr = new StreamReader(streamReceive, Encoding.UTF8)) { htmlCode = sr.ReadToEnd(); } } } if (webResponse != null) { webResponse.Close(); } if (webRequest != null) { webRequest.Abort(); } break; } catch (Exception e) //捕获所有异常 { LOGGER.WriteLine(e.Message); LOGGER.WriteLineError(e.Message); Thread.Sleep(1000); //1秒后重试 continue; } } return htmlCode; } [DllImport("shell32.dll", ExactSpelling = true)] private static extern void ILFree(IntPtr pidlList); [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] private static extern IntPtr ILCreateFromPathW(string pszPath); [DllImport("shell32.dll", ExactSpelling = true)] private static extern int SHOpenFolderAndSelectItems(IntPtr pidlList, uint cild, IntPtr children, uint dwFlags); //参数: // string dir 指定的文件夹 // string ext 文件类型的扩展名,如".txt" , “.exe" public static int GetFileCount(string dir, string ext) { if (!Directory.Exists(dir)) return 0; int count = 0; DirectoryInfo d = new DirectoryInfo(dir); foreach (FileInfo fi in d.GetFiles()) { if (fi.Extension.ToUpper() == ext.ToUpper()) { count++; } } return count; } /// /// 寻找指定目录下指定后缀的文件的详细路径 如".txt" /// /// /// /// public static string[] GetFiles(string dir, string ext) { ArrayList al = new ArrayList(); StringBuilder sb = new StringBuilder(); DirectoryInfo d = new DirectoryInfo(dir); foreach (FileInfo fi in d.GetFiles()) { if (fi.Extension.ToUpper() == ext.ToUpper()) { al.Add(fi.FullName); } } string[] res = (string[])al.ToArray(typeof(string)); Array.Sort(res); //排序 return res; } /// /// 获取url字符串参数,返回参数值字符串 /// /// 参数名称 /// url字符串 /// public static string GetQueryString(string name, string url) { Regex re = new Regex(@"(^|&)?(\w+)=([^&]+)(&|$)?", System.Text.RegularExpressions.RegexOptions.Compiled); MatchCollection mc = re.Matches(url); foreach (Match m in mc) { if (m.Result("$2").Equals(name)) { return m.Result("$3"); } } return ""; } //大量文件分部分二进制合并 public static void PartialCombineMultipleFiles(string[] files) { int div = 0; if (files.Length <= 90000) div = 100; else div = 200; string outputName = Path.GetDirectoryName(files[0]) + "\\T"; int index = 0; //序号 //按照div的容量分割为小数组 string[][] li = Enumerable.Range(0, files.Count() / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray(); foreach (var items in li) { if (items.Count() == 0) continue; CombineMultipleFilesIntoSingleFile(items, outputName + index.ToString("0000") + ".ts"); //合并后删除这些文件 foreach (var item in items) { File.Delete(item); } index++; } } /// /// 输入一堆已存在的文件,合并到新文件 /// /// /// public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath) { //同名文件已存在的共存策略 if (File.Exists(outputFilePath)) { outputFilePath = Path.Combine(Path.GetDirectoryName(outputFilePath), Path.GetFileNameWithoutExtension(outputFilePath) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + Path.GetExtension(outputFilePath)); } if (files.Length == 1) { FileInfo fi = new FileInfo(files[0]); fi.MoveTo(outputFilePath); return; } if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)); string[] inputFilePaths = files; using (var outputStream = File.Create(outputFilePath)) { foreach (var inputFilePath in inputFilePaths) { if (inputFilePath == "") continue; using (var inputStream = File.OpenRead(inputFilePath)) { // Buffer size can be passed as the second argument. inputStream.CopyTo(outputStream); } //Console.WriteLine("The file {0} has been processed.", inputFilePath); } } //Global.ExplorerFile(outputFilePath); } /// /// 将一个字节流附加至文件流 /// /// /// public static void AppendBytesToFileStreamAndDoNotClose(FileStream liveStream, byte[] file) { FileStream outputStream = liveStream; using (var inputStream = new MemoryStream(file)) { inputStream.CopyTo(outputStream); } } //重定向 public static string Get302(string url, string headers = "", int timeout = 5000) { try { string redirectUrl; WebRequest myRequest = WebRequest.Create(url); myRequest.Timeout = timeout; if (NoProxy) { myRequest.Proxy = null; } else if (UseProxyAddress != "") { SetProxy(myRequest); } //添加headers if (headers != "") { foreach (string att in headers.Split('|')) { try { myRequest.Headers.Add(att); } catch (Exception) { } } } WebResponse myResponse = myRequest.GetResponse(); redirectUrl = myResponse.ResponseUri.ToString(); myResponse.Close(); return redirectUrl; } catch (Exception) { return url; } } /// /// 下载文件为字节流 /// /// /// /// public static byte[] HttpDownloadFileToBytes(string url, string headers = "", int timeOut = 60000) { //本地文件 if (url.StartsWith("file:")) { Uri t = new Uri(url); url = t.LocalPath; if (File.Exists(url)) { FileStream fs = new FileStream(url, FileMode.Open, FileAccess.Read); byte[] infbytes = new byte[(int)fs.Length]; fs.Read(infbytes, 0, infbytes.Length); fs.Close(); return infbytes; } } reProcess: byte[] arraryByte; HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "GET"; req.Timeout = timeOut; req.ReadWriteTimeout = timeOut; //重要 req.AllowAutoRedirect = false; //手动处理重定向,否则会丢失Referer if (NoProxy) { req.Proxy = null; } else if (UseProxyAddress != "") { SetProxy(req); } req.Headers.Add("Accept-Encoding", "gzip, deflate"); req.Accept = "*/*"; req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"; //添加headers if (headers != "") { foreach (string att in headers.Split('|')) { try { if (att.Split(':')[0].ToLower() == "referer") req.Referer = att.Substring(att.IndexOf(":") + 1); else if (att.Split(':')[0].ToLower() == "user-agent") req.UserAgent = att.Substring(att.IndexOf(":") + 1); else if (att.Split(':')[0].ToLower() == "range") req.AddRange(Convert.ToInt32(att.Substring(att.IndexOf(":") + 1).Split('-')[0], Convert.ToInt32(att.Substring(att.IndexOf(":") + 1).Split('-')[1]))); else if (att.Split(':')[0].ToLower() == "accept") req.Accept = att.Substring(att.IndexOf(":") + 1); else req.Headers.Add(att); } catch (Exception e) { LOGGER.WriteLineError(e.Message); } } } using (HttpWebResponse wr = (HttpWebResponse)req.GetResponse()) { //302 if (wr.Headers.Get("Location") != null) { url = wr.Headers.Get("Location"); wr.Close(); goto reProcess; } if (wr.ContentEncoding != null && wr.ContentEncoding.ToLower() == "gzip") //如果使用了GZip则先解压 { using (Stream streamReceive = wr.GetResponseStream()) { using (var zipStream = new System.IO.Compression.GZipStream(streamReceive, System.IO.Compression.CompressionMode.Decompress)) { //读取到内存 MemoryStream stmMemory = new MemoryStream(); Stream responseStream = zipStream; byte[] bArr = new byte[1024]; int size = responseStream.Read(bArr, 0, (int)bArr.Length); while (size > 0) { stmMemory.Write(bArr, 0, size); size = responseStream.Read(bArr, 0, (int)bArr.Length); } arraryByte = stmMemory.ToArray(); responseStream.Close(); stmMemory.Close(); } } } else { using (Stream streamReceive = wr.GetResponseStream()) { //读取到内存 MemoryStream stmMemory = new MemoryStream(); Stream responseStream = streamReceive; byte[] bArr = new byte[1024]; int size = responseStream.Read(bArr, 0, (int)bArr.Length); while (size > 0) { stmMemory.Write(bArr, 0, size); size = responseStream.Read(bArr, 0, (int)bArr.Length); } arraryByte = stmMemory.ToArray(); responseStream.Close(); stmMemory.Close(); } } } return arraryByte; } /// /// Http下载文件 /// public static void HttpDownloadFile(string url, string path, int timeOut = 20000, string headers = "", long startByte = 0, long expectByte = -1) { int retry = 0; reDownload: try { if (File.Exists(path)) File.Delete(path); if (shouldStop) return; reProcess: HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; request.Timeout = timeOut; request.ReadWriteTimeout = timeOut; //重要 request.AllowAutoRedirect = false; //手动处理重定向,否则会丢失Referer request.KeepAlive = false; request.Method = "GET"; if (NoProxy) { request.Proxy = null; } else if (UseProxyAddress != "") { SetProxy(request); } if (url.Contains("data.video.iqiyi.com")) request.UserAgent = "QYPlayer/Android/4.4.5;NetType/3G;QTP/1.1.4.3"; else if (url.Contains("pcvideo") && url.Contains(".titan.mgtv.com")) { request.UserAgent = ""; if (!url.Contains("/internettv/")) request.Referer = "https://www.mgtv.com"; request.Headers.Add("Cookie", "MQGUID"); } else if (url.Contains(".xboku.com/")) //独播库 { request.Referer = "https://my.duboku.vip/static/player/videojs.html"; } else request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"; //下载部分字节 if (expectByte != -1) request.AddRange("bytes", startByte, startByte + expectByte - 1); //添加headers if (headers != "") { foreach (string att in headers.Split('|')) { try { if (att.Split(':')[0].ToLower() == "referer") request.Referer = att.Substring(att.IndexOf(":") + 1); else if (att.Split(':')[0].ToLower() == "user-agent") request.UserAgent = att.Substring(att.IndexOf(":") + 1); else if (att.Split(':')[0].ToLower() == "range") request.AddRange(Convert.ToInt32(att.Substring(att.IndexOf(":") + 1).Split('-')[0], Convert.ToInt32(att.Substring(att.IndexOf(":") + 1).Split('-')[1]))); else if (att.Split(':')[0].ToLower() == "accept") request.Accept = att.Substring(att.IndexOf(":") + 1); else request.Headers.Add(att); } catch (Exception e) { LOGGER.WriteLineError(e.Message); } } } long totalLen = 0; long downLen = 0; bool pngHeader = false; //PNG HEADER检测 using (var response = (HttpWebResponse)request.GetResponse()) { //302 if (response.Headers.Get("Location") != null) { url = response.Headers.Get("Location"); response.Close(); goto reProcess; } using (var responseStream = response.GetResponseStream()) { using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write)) { //responseStream.CopyTo(stream); totalLen = response.ContentLength; byte[] bArr = new byte[1024]; int size = responseStream.Read(bArr, 0, (int)bArr.Length); if (!pngHeader && size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3]) { pngHeader = true; } //GIF HEADER检测 if (!pngHeader && size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3]) { bArr = bArr.Skip(42).ToArray(); size -= 42; downLen += 42; } //BMP HEADER检测 if (!pngHeader && size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8]) { bArr = bArr.Skip(0x3E).ToArray(); size -= 0x3E; downLen += 0x3E; } while (size > 0) { stream.Write(bArr, 0, size); downLen += size; BYTEDOWN += size; //计算下载速度 if (MAX_SPEED != 0) while (BYTEDOWN >= MAX_SPEED * 1024 * DownloadManager.CalcTime) //限速 { Thread.Sleep(1); } size = responseStream.Read(bArr, 0, (int)bArr.Length); if (shouldStop) { request.Abort(); break; } } } } } if (shouldStop) try { File.Delete(path); } catch (Exception) { } if (totalLen != -1 && downLen != totalLen) try { File.Delete(path); } catch (Exception) { } if (pngHeader) TrySkipPngHeader(path); } catch (Exception e) { LOGGER.WriteLineError("DOWN: " + e.Message + " " + url); try { File.Delete(path); } catch (Exception) { } if (retry++ < 3) { Thread.Sleep(1000); LOGGER.WriteLineError($"DOWN: AUTO RETRY {retry}/3 " + url); goto reDownload; } } } /// /// 用于处理利用图床上传TS导致前面被插入PNG Header的情况 /// /// public static void TrySkipPngHeader(string filePath) { var u = File.ReadAllBytes(filePath); if (0x47 == u[0]) { return; } else if (u.Length > 120 && 137 == u[0] && 80 == u[1] && 78 == u[2] && 71 == u[3] && 96 == u[118] && 130 == u[119]) { u = u.Skip(120).ToArray(); } else if (u.Length > 6102 && 137 == u[0] && 80 == u[1] && 78 == u[2] && 71 == u[3] && 96 == u[6100] && 130 == u[6101]) { u = u.Skip(6102).ToArray(); } else if (u.Length > 69 && 137 == u[0] && 80 == u[1] && 78 == u[2] && 71 == u[3] && 96 == u[67] && 130 == u[68]) { u = u.Skip(69).ToArray(); } else if (u.Length > 771 && 137 == u[0] && 80 == u[1] && 78 == u[2] && 71 == u[3] && 96 == u[769] && 130 == u[770]) { u = u.Skip(771).ToArray(); } else if (u.Length > 4 && 137 == u[0] && 80 == u[1] && 78 == u[2] && 71 == u[3]) { //确定是PNG但是需要手动查询结尾标记 0x47 出现两次 int skip = 0; for (int i = 4; i < u.Length - 188 * 2 - 4; i++) { if (u[i] == 0x47 && u[i + 188] == 0x47 && u[i + 188 + 188] == 0x47) { skip = i; break; } } u = u.Skip(skip).ToArray(); } File.WriteAllBytes(filePath, u); } //格式化json字符串 public static string ConvertJsonString(string str) { //Console.WriteLine(str); JsonSerializer serializer = new JsonSerializer(); TextReader tr = new StringReader(str); JsonTextReader jtr = new JsonTextReader(tr); object obj = serializer.Deserialize(jtr); if (obj != null) { StringWriter textWriter = new StringWriter(); JsonTextWriter jsonWriter = new JsonTextWriter(textWriter) { Formatting = Newtonsoft.Json.Formatting.Indented, Indentation = 2, IndentChar = ' ' }; //Indentation 为缩进量 serializer.Serialize(jsonWriter, obj); return textWriter.ToString(); } else { return str; } } //获取属性 public static string GetTagAttribute(string attributeList, string key) { /*#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1056x594,BANDWIDTH=1963351,CODECS="mp4a.40.5,avc1.4d001f",FRAME-RATE=30.000,AUDIO="aac",AVERAGE-BANDWIDTH=1655131*/ if (attributeList != "") { try { string tmp = attributeList.Trim(); if (tmp.Contains(key + "=")) { if (tmp[tmp.IndexOf(key + "=") + key.Length + 1] == '\"') { return tmp.Substring(tmp.IndexOf(key + "=") + key.Length + 2, tmp.Remove(0, tmp.IndexOf(key + "=") + key.Length + 2).IndexOf('\"')); } else { if (tmp.Remove(0, tmp.IndexOf(key + "=") + key.Length + 2).Contains(",")) return tmp.Substring(tmp.IndexOf(key + "=") + key.Length + 1, tmp.Remove(0, tmp.IndexOf(key + "=") + key.Length + 1).IndexOf(',')); else return tmp.Substring(tmp.IndexOf(key + "=") + key.Length + 1); } } } catch (Exception) { return string.Empty; } } return string.Empty; } //正则表达式 public static ArrayList RegexFind(string regex, string src, int group = -1) { ArrayList array = new ArrayList(); Regex reg = new Regex(@regex); MatchCollection result = reg.Matches(src); if (result.Count == 0) array.Add("NULL"); foreach (Match m in result) { if (group == -1) array.Add(m.Value); else array.Add(m.Groups[group].Value); } return array; } //调用ffmpeg获取视频信息 public static ArrayList GetVideoInfo(string file) { LOGGER.WriteLine(strings.readingFileInfo); LOGGER.PrintLine(strings.readingFileInfo, LOGGER.Warning); StringBuilder sb = new StringBuilder(); ArrayList info = new ArrayList(); string cmd = "-hide_banner -i \"" + file + "\""; if (!File.Exists(file)) { info.Add("Error in reading file"); return info; } using (Process p = new Process()) { p.StartInfo.FileName = "ffmpeg"; p.StartInfo.Arguments = cmd; p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动 p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息 p.StartInfo.RedirectStandardOutput = true; //由调用程序获取输出信息 p.StartInfo.RedirectStandardError = true; //重定向标准错误输出 p.StartInfo.CreateNoWindow = true; //不显示程序窗口 p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.Start();//启动程序 p.StandardInput.AutoFlush = true; //获取cmd窗口的输出信息 StreamReader reader = p.StandardError;//截取输出流 sb.Append(reader.ReadLine() + "\r\n");//每次读取一行 while (!reader.EndOfStream) { sb.Append(reader.ReadLine() + "\r\n"); } p.WaitForExit();//等待程序执行完退出进程 p.Close(); } string res = string.Empty; foreach (string s in (string[])RegexFind("Stream #.*", sb.ToString()).ToArray(typeof(string))) { res = "PID " + RegexFind(@"\[(0x\d{2,})\]", s, 1)[0].ToString() + ": " + RegexFind(@": (.*)", s, 1)[0].ToString() .Replace(RegexFind(@" \(\[.*?\)", s)[0].ToString(), "") .Replace(": ", " "); if (VIDEO_TYPE == "" && res.Contains(": Video")) { if (res.Contains("Video dvvideo")) //爱奇艺杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video none (dvhe")) //腾讯视频杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video hevc (dvhe")) //腾讯视频杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video hevc (DOVI")) //腾讯视频杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video hevc (Main 10) (DOVI")) //优酷视频杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video hevc (Main 10) (dvh1")) //优酷视频杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video hevc (dvh1")) //优酷视频杜比视界 { VIDEO_TYPE = "DV"; } else if (res.Contains("Video h264")) { VIDEO_TYPE = "H264"; } else if (res.Contains("Video hevc")) { VIDEO_TYPE = "H265"; } else { VIDEO_TYPE = "UNKOWN"; } } if (res.Contains("Audio aac")) { FFmpeg.UseAACFilter = true; } //有非AAC音轨则关闭UseAACFilter if (res.Contains("Audio") && !res.Contains("Audio aac")) { FFmpeg.UseAACFilter = false; } if ((VIDEO_TYPE == "" || VIDEO_TYPE == "IGNORE") && res.Contains("Audio eac3")) { AUDIO_TYPE = "eac3"; } else if((VIDEO_TYPE == "" || VIDEO_TYPE == "IGNORE") && res.Contains("Audio aac")) { AUDIO_TYPE = "aac"; } else if ((VIDEO_TYPE == "" || VIDEO_TYPE == "IGNORE") && res.Contains("Audio ac3")) { AUDIO_TYPE = "ac3"; } info.Add(res); } if (VIDEO_TYPE != "" && VIDEO_TYPE != "IGNORE") AUDIO_TYPE = ""; return info; } //所给路径中所对应的文件大小 public static long FileSize(string filePath) { //定义一个FileInfo对象,是指与filePath所指向的文件相关联,以获取其大小 FileInfo fileInfo = new FileInfo(filePath); return fileInfo.Length; } //获取文件夹大小 public static long GetDirectoryLength(string path) { if (!Directory.Exists(path)) { return 0; } long size = 0; //遍历指定路径下的所有文件 DirectoryInfo di = new DirectoryInfo(path); foreach (FileInfo fi in di.GetFiles()) { size += fi.Length; } //遍历指定路径下的所有文件夹 DirectoryInfo[] dis = di.GetDirectories(); if (dis.Length > 0) { for (int i = 0; i < dis.Length; i++) { size += GetDirectoryLength(dis[i].FullName); } } return size; } //此函数用于格式化输出时长 public static String FormatTime(Int32 time) { TimeSpan ts = new TimeSpan(0, 0, time); string str = ""; str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s"; return str; } //此函数用于格式化输出文件大小 public static String FormatFileSize(Double fileSize) { if (fileSize < 0) { return "Error"; } else if (fileSize >= 1024 * 1024 * 1024) { return string.Format("{0:########0.00} GB", ((Double)fileSize) / (1024 * 1024 * 1024)); } else if (fileSize >= 1024 * 1024) { return string.Format("{0:####0.00} MB", ((Double)fileSize) / (1024 * 1024)); } else if (fileSize >= 1024) { return string.Format("{0:####0.00} KB", ((Double)fileSize) / 1024); } else { return string.Format("{0} bytes", fileSize); } } /// /// 获取当前时间戳 /// /// 为真时获取10位时间戳,为假时获取13位时间戳.bool bflag = true /// public static string GetTimeStamp(bool bflag) { TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); string ret = string.Empty; if (bflag) ret = Convert.ToInt64(ts.TotalSeconds).ToString(); else ret = Convert.ToInt64(ts.TotalMilliseconds).ToString(); return ret; } /// /// 获取有效文件名 /// /// /// /// public static string MakeValidFileName(string text, string replacement = "_") { StringBuilder str = new StringBuilder(); var invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (var c in text) { if (invalidFileNameChars.Contains(c)) { str.Append(replacement ?? ""); } else { str.Append(c); } } return str.ToString(); } /// /// 从URL获取文件名 /// /// /// public static string GetUrlFileName(string url) { if (File.Exists(url)) { return Path.GetFileNameWithoutExtension(url); } if (string.IsNullOrEmpty(url)) { return "None"; } try { string[] strs1 = url.Split(new char[] { '/' }); return MakeValidFileName(System.Web.HttpUtility.UrlDecode(strs1[strs1.Length - 1].Split(new char[] { '?' })[0].Replace(".m3u8", ""))); } catch (Exception) { return DateTime.Now.ToString("yyyy.MM.dd-HH.mm.ss"); } } //检测GZip并解压 public static void GzipHandler(string file) { try { using (FileStream fr = File.OpenRead(file)) { using (GZipStream gz = new GZipStream(fr, CompressionMode.Decompress)) { using (FileStream fw = File.OpenWrite(Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + "[t].ts"))) { byte[] by = new byte[1024]; int r = gz.Read(by, 0, by.Length); while (r > 0) { fw.Write(by, 0, r); r = gz.Read(by, 0, r); } } } File.Delete(file); File.Move(Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + "[t].ts"), file); } } catch (Exception) { if (File.Exists(Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + "[t].ts"))) File.Delete(Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + "[t].ts")); return; } } [DllImport("shell32.dll", SetLastError = true)] static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); //使用Win32 API解析字符串为命令行参数 public static IEnumerable ParseArguments(string commandLine) { int argc; var argv = CommandLineToArgvW(commandLine, out argc); if (argv == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(); try { var args = new string[argc]; for (var i = 0; i < args.Length; i++) { var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size); args[i] = Marshal.PtrToStringUni(p); } return args; } finally { Marshal.FreeHGlobal(argv); } } //重载 public class WebClientEx : WebClient { private readonly long from; private readonly long to; private readonly int timeout; private readonly bool setTimeout; private readonly bool setRange; public WebClientEx() { } public WebClientEx(long from, long to) { this.from = from; this.to = to; setRange = true; } public WebClientEx(int timeout) { this.timeout = timeout; setTimeout = true; } public WebClientEx(int timeout, long from, long to) { this.timeout = timeout; setTimeout = true; this.from = from; this.to = to; setRange = true; } protected override WebRequest GetWebRequest(Uri address) { var wr = (HttpWebRequest)base.GetWebRequest(address); if (NoProxy) { wr.Proxy = null; } else if (UseProxyAddress != "") { SetProxy(wr); } if (setRange) wr.AddRange(this.from, this.to); if (setTimeout) wr.Timeout = timeout; // timeout in milliseconds (ms) return wr; } } /** * 通过X-TIMESTAMP-MAP 调整VTT字幕的时间轴 */ public static void ReAdjustVtt(string[] vtts) { string MsToTime(int ms) { TimeSpan ts = new TimeSpan(0, 0, 0, 0, ms); string str = ""; str = (ts.Hours.ToString("00") + ":") + ts.Minutes.ToString("00") + ":" + ts.Seconds.ToString("00") + "." + ts.Milliseconds.ToString("000"); return str; } int TimeToMs(string line) { int hh = Convert.ToInt32(line.Split(':')[0]); int mm = Convert.ToInt32(line.Split(':')[1]); int ss = Convert.ToInt32(line.Split(':')[2].Split('.')[0]); int ms = Convert.ToInt32(line.Split(':')[2].Split('.')[1]); return hh * 60 * 60 * 1000 + mm * 60 * 1000 + ss * 1000 + ms; } int addTime = 0; int baseTime = 0; for (int i = 0; i < vtts.Length; i++) { string tmp = File.ReadAllText(vtts[i], Encoding.UTF8); if (!Regex.IsMatch(tmp, "X-TIMESTAMP-MAP.*MPEGTS:(\\d+)")) break; if (i > 0) { int newTime = Convert.ToInt32(Regex.Match(tmp, "X-TIMESTAMP-MAP.*MPEGTS:(\\d+)").Groups[1].Value); if (newTime == 900000) continue; //计算偏移量 //LOGGER.PrintLine((newTime - baseTime).ToString()); addTime = addTime + ((newTime - baseTime) / 100); if ((newTime - baseTime) == 6300000) addTime -= 3000; //将新的作为基准时间 baseTime = newTime; foreach (Match m in Regex.Matches(tmp, @"(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})")) { string start = m.Groups[1].Value; string end = m.Groups[2].Value; tmp = tmp.Replace(m.Value, MsToTime(TimeToMs(start) + addTime) + " --> " + MsToTime(TimeToMs(end) + addTime)); } } File.WriteAllText(vtts[i], Regex.Replace(tmp, "X-TIMESTAMP-MAP=.*", ""), Encoding.UTF8); } //Console.ReadLine(); } } } ================================================ FILE: N_m3u8DL-CLI/HLSLiveDownloader.cs ================================================ using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Timers; namespace N_m3u8DL_CLI { class HLSLiveDownloader { public static int REC_DUR_LIMIT = -1; //默认不限制录制时长 public static double REC_DUR = 0; //已录制时长 private string liveFile = string.Empty; private string jsonFile = string.Empty; private string headers = string.Empty; private string downDir = string.Empty; private FileStream liveStream = null; private double targetduration = 10; private bool isFirstJson = true; public double TotalDuration { get; set; } public string Headers { get => headers; set => headers = value; } public string DownDir { get => downDir; set => downDir = value; } public FileStream LiveStream { get => liveStream; set => liveStream = value; } public string LiveFile { get => liveFile; set => liveFile = value; } ArrayList toDownList = new ArrayList(); //所有待下载的列表 System.Timers.Timer timer = new System.Timers.Timer(); Downloader sd = new Downloader(); //只有一个实例 public void TimerStart() { timer.Enabled = true; //timer.Interval = (targetduration - 2) * 1000; //执行间隔时间,单位为毫秒 timer.Start(); timer.Elapsed += new ElapsedEventHandler(UpdateList); UpdateList(timer, new EventArgs()); //立即执行一次 Record(); } public void TimerStop() { timer.Stop(); } //更新列表 private void UpdateList(object source, EventArgs e) { jsonFile = Path.Combine(DownDir, "meta.json"); if (!File.Exists(jsonFile)) { TimerStop(); return; } string jsonContent = File.ReadAllText(jsonFile); JObject initJson = JObject.Parse(jsonContent); string m3u8Url = initJson["m3u8"].Value(); targetduration = initJson["m3u8Info"]["targetDuration"].Value(); TotalDuration = initJson["m3u8Info"]["totalDuration"].Value(); timer.Interval = Math.Abs(TotalDuration - targetduration) * 1000;//设置定时器运行间隔 if (timer.Interval <= 1000) timer.Interval = 10000; JArray lastSegments = JArray.Parse(initJson["m3u8Info"]["segments"][0].ToString().Trim()); //上次的分段,用于比对新分段 ArrayList tempList = new ArrayList(); //所有待下载的列表 tempList.Clear(); foreach (JObject seg in lastSegments) { tempList.Add(seg.ToString()); } if(isFirstJson) { toDownList = tempList; isFirstJson = false; return; } Parser parser = new Parser(); parser.Headers = Headers; parser.DownDir = Path.GetDirectoryName(jsonFile); parser.M3u8Url = m3u8Url; parser.LiveStream = true; parser.Parse(); //产生新的json文件 jsonContent = File.ReadAllText(jsonFile); initJson = JObject.Parse(jsonContent); JArray segments = JArray.Parse(initJson["m3u8Info"]["segments"][0].ToString()); //大分组 foreach (JObject seg in segments) { if (!tempList.Contains(seg.ToString())) { toDownList.Add(seg.ToString()); //加入真正的待下载队列 //Console.WriteLine(seg.ToString()); } } if (toDownList.Count > 0) Record(); } //public void TryDownload() //{ // Thread t = new Thread(Download); // while (toDownList.Count != 0) // { // t = new Thread(Download); // t.Start(); // t.Join(); // while (sd.IsDone != true) ; //忙等待 // if (toDownList.Count > 0) // toDownList.RemoveAt(0); //下完删除一项 // } // Console.WriteLine("Waiting..."); //} private void Record() { while (toDownList.Count > 0 && (sd.FileUrl != "" ? sd.IsDone : true)) { JObject info = JObject.Parse(toDownList[0].ToString()); int index = info["index"].Value(); sd.FileUrl = info["segUri"].Value(); sd.Method = info["method"].Value(); if (sd.Method != "NONE") { sd.Key = info["key"].Value(); sd.Iv = info["iv"].Value(); } sd.TimeOut = (int)timer.Interval - 1000;//超时时间不超过下次执行时间 if (sd.TimeOut <= 0) sd.TimeOut = (int)timer.Interval; sd.SegIndex = index; sd.Headers = Headers; sd.SegDur = info["duration"].Value(); sd.IsLive = true; //标记为直播 sd.LiveFile = LiveFile; sd.LiveStream = LiveStream; sd.Down(); //开始下载 while (sd.IsDone != true) { Thread.Sleep(1); }; //忙等待 Thread.Sleep(1) 可防止cpu 100% 防止电脑风扇狂转 if (toDownList.Count > 0) toDownList.RemoveAt(0); //下完删除一项 } LOGGER.PrintLine("Waiting...", LOGGER.Warning); LOGGER.WriteLine("Waiting..."); } //检测是否有新分片 private bool isNewSeg() { if (toDownList.Count > 0) return true; return false; } } } ================================================ FILE: N_m3u8DL-CLI/HLSTags.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace N_m3u8DL_CLI { class HLSTags { public static string ext_m3u = "#EXTM3U"; public static string ext_x_targetduration = "#EXT-X-TARGETDURATION"; public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; public static string ext_x_media = "#EXT-X-MEDIA"; public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; public static string ext_x_key = "#EXT-X-KEY"; public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; public static string ext_x_version = "#EXT-X-VERSION"; public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; public static string ext_x_endlist = "#EXT-X-ENDLIST"; public static string extinf = "#EXTINF"; public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; public static string ext_x_byterange = "#EXT-X-BYTERANGE"; public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; public static string ext_x_cue_end = "#EXT-X-CUE-IN"; public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; public static string ext_x_map = "#EXT-X-MAP"; public static string ext_x_start = "#EXT-X-START"; } } ================================================ FILE: N_m3u8DL-CLI/IqJsonParser.cs ================================================ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace N_m3u8DL_CLI { class IqJsonParser { public static string Parse(string downDir, string json) { JObject jObject = JObject.Parse(json); var aClips = jObject["payload"]["wm_a"]["audio_track1"]["files"].Value(); var vClips = jObject["payload"]["wm_a"]["video_track1"]["files"].Value(); var codecsList = new List(); var audioPath = ""; var videoPath = ""; var audioInitPath = ""; var videoInitPath = ""; if (aClips.Count > 0) { var init = jObject["payload"]["wm_a"]["audio_track1"]["codec_init"].Value(); byte[] bytes = Convert.FromBase64String(init); //输出init文件 audioInitPath = Path.Combine(downDir, "iqAudioInit.mp4"); File.WriteAllBytes(audioInitPath, bytes); StringBuilder sb = new StringBuilder(); sb.AppendLine("#EXTM3U"); sb.AppendLine("#EXT-X-VERSION:3"); sb.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); sb.AppendLine("#CREATED-BY:N_m3u8DL-CLI"); sb.AppendLine($"#EXT-CODEC:{jObject["payload"]["wm_a"]["audio_track1"]["codec"].Value()}"); sb.AppendLine($"#EXT-KID:{jObject["payload"]["wm_a"]["audio_track1"]["key_id"].Value()}"); sb.AppendLine($"#EXT-X-MAP:URI=\"{new Uri(Path.Combine(downDir + "(Audio)", "iqAudioInit.mp4")).ToString()}\""); sb.AppendLine("#EXT-X-KEY:METHOD=PLZ-KEEP-RAW,URI=\"None\""); foreach (var a in aClips) { sb.AppendLine($"#EXTINF:{a["duration_second"].ToString()}"); sb.AppendLine(a["file_name"].Value()); } sb.AppendLine("#EXT-X-ENDLIST"); //输出m3u8文件 var _path = Path.Combine(downDir, "iqAudio.m3u8"); File.WriteAllText(_path, sb.ToString()); audioPath = new Uri(_path).ToString(); codecsList.Add(jObject["payload"]["wm_a"]["audio_track1"]["codec"].Value()); } if (vClips.Count > 0) { var init = jObject["payload"]["wm_a"]["video_track1"]["codec_init"].Value(); byte[] bytes = Convert.FromBase64String(init); //输出init文件 videoInitPath = Path.Combine(downDir, "iqVideoInit.mp4"); File.WriteAllBytes(videoInitPath, bytes); StringBuilder sb = new StringBuilder(); sb.AppendLine("#EXTM3U"); sb.AppendLine("#EXT-X-VERSION:3"); sb.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); sb.AppendLine("#CREATED-BY:N_m3u8DL-CLI"); sb.AppendLine($"#EXT-CODEC:{jObject["payload"]["wm_a"]["video_track1"]["codec"].Value()}"); sb.AppendLine($"#EXT-KID:{jObject["payload"]["wm_a"]["video_track1"]["key_id"].Value()}"); sb.AppendLine($"#EXT-X-MAP:URI=\"{new Uri(videoInitPath).ToString()}\""); sb.AppendLine("#EXT-X-KEY:METHOD=PLZ-KEEP-RAW,URI=\"None\""); foreach (var a in vClips) { var start = a["seekable"]["pos_start"].Value(); var size = a["size"].Value(); sb.AppendLine($"#EXTINF:{a["duration_second"].ToString()}"); sb.AppendLine($"#EXT-X-BYTERANGE:{size}@{start}"); sb.AppendLine(a["file_name"].Value()); } sb.AppendLine("#EXT-X-ENDLIST"); //输出m3u8文件 var _path = Path.Combine(downDir, "iqVideo.m3u8"); File.WriteAllText(_path, sb.ToString()); videoPath = new Uri(_path).ToString(); codecsList.Add(jObject["payload"]["wm_a"]["video_track1"]["codec"].Value()); } var content = ""; if ((videoPath == "" && audioPath != "") || Global.VIDEO_TYPE == "IGNORE") { return audioPath; } else if (audioPath == "" && videoPath != "") { return videoPath; } else { if (!Directory.Exists(downDir + "(Audio)")) Directory.CreateDirectory(downDir + "(Audio)"); var _path = Path.Combine(downDir + "(Audio)", "iqAudio.m3u8"); var _pathInit = Path.Combine(downDir + "(Audio)", "iqAudioInit.mp4"); File.Copy(new Uri(audioPath).LocalPath, _path, true); File.Copy(new Uri(audioInitPath).LocalPath, _pathInit, true); audioPath = new Uri(_path).ToString(); content = $"#EXTM3U\r\n" + $"#EXT-X-MEDIA:TYPE=AUDIO,URI=\"{audioPath}\",GROUP-ID=\"default-audio-group\",NAME=\"stream_0\",AUTOSELECT=YES,CHANNELS=\"0\"\r\n" + $"#EXT-X-STREAM-INF:BANDWIDTH=99999,CODECS=\"{string.Join(",", codecsList)}\",RESOLUTION=0x0,AUDIO=\"default-audio-group\"\r\n" + $"{videoPath}"; } var _masterPath = Path.Combine(downDir, "master.m3u8"); File.WriteAllText(_masterPath, content); return new Uri(_masterPath).ToString(); } } } ================================================ FILE: N_m3u8DL-CLI/LOGGER.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace N_m3u8DL_CLI { class LOGGER { public const int Default = 1; public const int Error = 2; public const int Warning = 3; public static string LOGFILE; public static bool STOPLOG = false; public static string FindLog(string dir) { DirectoryInfo d = new DirectoryInfo(dir); foreach (FileInfo fi in d.GetFiles()) { if (fi.Extension.ToUpper() == ".LOG") { return fi.FullName; } } return ""; } public static void InitLog() { if (!Directory.Exists(Path.GetDirectoryName(LOGFILE)))//若文件夹不存在则新建文件夹 Directory.CreateDirectory(Path.GetDirectoryName(LOGFILE)); //新建文件夹 //若文件存在则加序号 int index = 1; var fileName = Path.GetFileNameWithoutExtension(LOGFILE); while (File.Exists(LOGFILE)) { LOGFILE = Path.Combine(Path.GetDirectoryName(LOGFILE), $"{fileName}-{index++}.log"); } string file = LOGFILE; string now = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string init = "LOG " + DateTime.Now.ToString("yyyy/MM/dd") + "\r\n" + "Save Path: " + Path.GetDirectoryName(LOGFILE) + "\r\n" + "Task Start: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + "\r\n" + "Task CommandLine: " + Environment.CommandLine; if (File.Exists(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), "N_m3u8DL-CLI.args.txt"))) { init += "\r\nAdditional Args: " + File.ReadAllText(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), "N_m3u8DL-CLI.args.txt")); //解析命令行 } init += "\r\n\r\n"; File.WriteAllText(file, init, Encoding.UTF8); } //读写锁机制,当资源被占用,其他线程等待 static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); public static void PrintLine(string text, int printLevel = 1) { int windowWith = 63; try { windowWith = Console.WindowWidth; } catch (Exception e) { // empty } switch (printLevel) { case 0: Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); Console.WriteLine(" ".PadRight(12) + " " + text); break; case 1: Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " "); Console.WriteLine(text); break; case 2: Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " "); Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(text); Console.ResetColor(); break; case 3: Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " "); Console.ForegroundColor = ConsoleColor.DarkYellow; Console.WriteLine(text); Console.ResetColor(); break; } } public static void WriteLine(string text) { if (STOPLOG) return; if (!File.Exists(LOGFILE)) return; try { string file = LOGFILE; //进入写入 LogWriteLock.EnterWriteLock(); using (StreamWriter sw = File.AppendText(file)) { sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " / (NORMAL) " + text, Encoding.UTF8); } } catch (Exception) { } finally { //释放占用 LogWriteLock.ExitWriteLock(); } } public static void WriteLineError(string text) { if (!File.Exists(LOGFILE)) return; try { string file = LOGFILE; //进入写入 LogWriteLock.EnterWriteLock(); using (StreamWriter sw = File.AppendText(file)) { sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " / (ERROR) " + text, Encoding.UTF8); } } catch (Exception) { } finally { //释放占用 LogWriteLock.ExitWriteLock(); } } public static void Show(string text) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(DateTime.Now.ToString("o") + " " + text); while (Console.ForegroundColor == ConsoleColor.Red) Console.ResetColor(); } } } ================================================ FILE: N_m3u8DL-CLI/MPDParser.cs ================================================ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; namespace N_m3u8DL_CLI { //code from https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py#L2076 class MPDParser { private static string MPD_URL; static Dictionary ExtractMultisegmentInfo(XmlElement Period, XmlNamespaceManager nsMgr, Dictionary info) { var MultisegmentInfo = new Dictionary(info); void ExtractCommon(XmlNode source) { var sourceE = (XmlElement)source; var segmentTimeline = source.SelectSingleNode("ns:SegmentTimeline", nsMgr); if (segmentTimeline != null) { var sE = segmentTimeline.SelectNodes("ns:S", nsMgr); if (sE.Count > 0) { MultisegmentInfo["TotalNumber"] = 0; var SList = new List>(); foreach (XmlElement s in sE) { var r = string.IsNullOrEmpty(s.GetAttribute("r")) ? 0 : Convert.ToInt64(s.GetAttribute("r")); MultisegmentInfo["TotalNumber"] += 1 + r; SList.Add(new Dictionary() { ["t"] = string.IsNullOrEmpty(s.GetAttribute("t")) ? 0 : Convert.ToInt64(s.GetAttribute("t")), ["d"] = Convert.ToInt64(s.GetAttribute("d")), ["r"] = r }); } MultisegmentInfo.Add("S", SList); } } var startNumber = sourceE.GetAttribute("startNumber"); if (!string.IsNullOrEmpty(startNumber)) { MultisegmentInfo["StartNumber"] = Convert.ToInt32(startNumber); } var timescale = sourceE.GetAttribute("timescale"); if (!string.IsNullOrEmpty(timescale)) { MultisegmentInfo["Timescale"] = Convert.ToInt32(timescale); } var segmentDuration = sourceE.GetAttribute("duration"); if (!string.IsNullOrEmpty(segmentDuration)) { MultisegmentInfo["SegmentDuration"] = Convert.ToDouble(segmentDuration); } } void ExtractInitialization(XmlNode source) { var initialization = source.SelectSingleNode("ns:Initialization", nsMgr); if (initialization != null) { MultisegmentInfo["InitializationUrl"] = ((XmlElement)initialization).GetAttribute("sourceURL"); if (((XmlElement)initialization).HasAttribute("range")) { MultisegmentInfo["InitializationUrl"] += "$$Range=" + ((XmlElement)initialization).GetAttribute("range"); } } } var segmentList = Period.SelectSingleNode("ns:SegmentList", nsMgr); if (segmentList != null) { ExtractCommon(segmentList); ExtractInitialization(segmentList); var segmentUrlsE = segmentList.SelectNodes("ns:SegmentURL", nsMgr); MultisegmentInfo["SegmentUrls"] = new List(); foreach (XmlElement segment in segmentUrlsE) { if (segment.HasAttribute("mediaRange")) { MultisegmentInfo["SegmentUrls"].Add("$$Range=" + segment.GetAttribute("mediaRange")); } else { MultisegmentInfo["SegmentUrls"].Add(segment.GetAttribute("media")); } } } else { var segmentTemplate = Period.SelectSingleNode("ns:SegmentTemplate", nsMgr); if (segmentTemplate != null) { ExtractCommon(segmentTemplate); var media = ((XmlElement)segmentTemplate).GetAttribute("media"); if (!string.IsNullOrEmpty(media)) { MultisegmentInfo["Media"] = media; } var initialization = ((XmlElement)segmentTemplate).GetAttribute("initialization"); if (!string.IsNullOrEmpty(initialization)) { MultisegmentInfo["Initialization"] = initialization; } else { ExtractInitialization(segmentTemplate); } } } return MultisegmentInfo; } /// /// 返回生成的master文件地址 /// /// 文件存储目录 /// MPD链接 /// MPD内容 /// BaseUrl /// public static string Parse(string downDir, string mpdUrl, string mpdContent, string defaultBase = "") { MPD_URL = mpdUrl; //XiGua if (mpdContent.Contains(",并替换本身的URL var baseNode = xn.SelectSingleNode("ns:BaseURL", nsMgr); if (baseNode != null) { if (MPD_URL.Contains("kkbox.com.tw/")) { var badUrl = baseNode.InnerText; var goodUrl = badUrl.Replace("//https:%2F%2F", "//"); MPD_URL = mpdUrl = goodUrl; } else { MPD_URL = mpdUrl = baseNode.InnerText; } } var formatList = new List>(); //存放所有音视频清晰度 var periodIndex = 0; //解决同一个period且同id导致被重复添加分片 foreach (XmlElement period in xn.SelectNodes("ns:Period", nsMgr)) { periodIndex++; var periodDuration = string.IsNullOrEmpty(period.GetAttribute("duration")) ? XmlConvert.ToTimeSpan(mediaPresentationDuration) : XmlConvert.ToTimeSpan(period.GetAttribute("duration")); var periodMsInfo = ExtractMultisegmentInfo(period, nsMgr, new Dictionary() { ["StartNumber"] = 1, ["Timescale"] = 1 }); foreach (XmlElement adaptationSet in period.SelectNodes("ns:AdaptationSet", nsMgr)) { var adaptionSetMsInfo = ExtractMultisegmentInfo(adaptationSet, nsMgr, periodMsInfo); foreach (XmlElement representation in adaptationSet.SelectNodes("ns:Representation", nsMgr)) { string GetAttribute(string key) { var v1 = representation.GetAttribute(key); if (string.IsNullOrEmpty(v1)) return adaptationSet.GetAttribute(key); return v1; } var mimeType = GetAttribute("mimeType"); var contentType = mimeType.Split('/')[0]; if (contentType == "text") { continue; //暂不支持字幕下载 } else if (contentType == "video" || contentType == "audio") { var baseUrl = ""; bool CheckBaseUrl() { return Regex.IsMatch(baseUrl, @"^https?://"); } var list = new List() { representation.ChildNodes, adaptationSet.ChildNodes, period.ChildNodes, mpdDoc.ChildNodes }; foreach (XmlNodeList xmlNodeList in list) { foreach (XmlNode node in xmlNodeList) { if (node.Name == "BaseURL") { baseUrl = node.InnerText + baseUrl; if (CheckBaseUrl()) break; } } if (CheckBaseUrl()) break; } string GetBaseUrl(string url) { if (url.Contains("?")) url = url.Remove(url.LastIndexOf('?')); url = url.Substring(0, url.LastIndexOf('/') + 1); return url; } var mpdBaseUrl = string.IsNullOrEmpty(defaultBase) ? GetBaseUrl(mpdUrl) : defaultBase; if (!string.IsNullOrEmpty(mpdBaseUrl) && !CheckBaseUrl()) { if (!mpdBaseUrl.EndsWith("/") && !baseUrl.StartsWith("/")) { mpdBaseUrl += "/"; } baseUrl = CombineURL(mpdBaseUrl, baseUrl); } var representationId = GetAttribute("id"); var lang = GetAttribute("lang"); var bandwidth = IntOrNull(GetAttribute("bandwidth")); var f = new Dictionary { ["PeriodIndex"] = periodIndex, ["ContentType"] = contentType, ["FormatId"] = representationId, ["ManifestUrl"] = mpdUrl, ["Width"] = IntOrNull(GetAttribute("width")), ["Height"] = IntOrNull(GetAttribute("height")), ["Tbr"] = DoubleOrNull(bandwidth, 1000), ["Asr"] = IntOrNull(GetAttribute("audioSamplingRate")), ["Fps"] = IntOrNull(GetAttribute("frameRate")), ["Language"] = lang, ["Codecs"] = GetAttribute("codecs") }; var representationMsInfo = ExtractMultisegmentInfo(representation, nsMgr, adaptionSetMsInfo); string PrepareTemplate(string templateName, string[] identifiers) { var tmpl = representationMsInfo[templateName]; var t = new StringBuilder(); var inTemplate = false; foreach (var ch in tmpl) { t.Append(ch); if (ch == '$') { inTemplate = !inTemplate; } else if (ch == '%' && !inTemplate) { t.Append(ch); } } var str = t.ToString(); str = str.Replace("$RepresentationID$", representationId); str = Regex.Replace(str, "\\$(" + string.Join("|", identifiers) + ")\\$", "{{$1}}"); str = Regex.Replace(str, "\\$(" + string.Join("|", identifiers) + ")%([^$]+)d\\$", "{{$1}}{0:D$2}"); str = str.Replace("$$", "$"); return str; } string PadNumber(string template, string key, long value) { string ReplaceFirst(string text, string search, string replace) { int pos = text.IndexOf(search); if (pos < 0) { return text; } return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); } template = template.Replace("{{" + key + "}}", ""); var m = Regex.Match(template, "{0:D(\\d+)}"); return ReplaceFirst(template, m.Value, value.ToString("0".PadRight(Convert.ToInt32(m.Groups[1].Value), '0'))); } if (representationMsInfo.ContainsKey("Initialization")) { var initializationTemplate = PrepareTemplate("Initialization", new string[] { "Bandwidth" }); var initializationUrl = ""; if (initializationTemplate.Contains("{0:D")) { if (initializationTemplate.Contains("{{Bandwidth}}")) initializationUrl = PadNumber(initializationTemplate, "Bandwidth", bandwidth); } else { initializationUrl = initializationTemplate.Replace("{{Bandwidth}}", bandwidth.ToString()); } representationMsInfo["InitializationUrl"] = CombineURL(baseUrl, initializationUrl); } string LocationKey(string location) { return Regex.IsMatch(location, "^https?://") ? "url" : "path"; } if (!representationMsInfo.ContainsKey("SegmentUrls") && representationMsInfo.ContainsKey("Media")) { var mediaTemplate = PrepareTemplate("Media", new string[] { "Number", "Bandwidth", "Time" }); var mediaLocationKey = LocationKey(mediaTemplate); if (mediaTemplate.Contains("{{Number") && !representationMsInfo.ContainsKey("S")) { var segmentDuration = 0.0; if (!representationMsInfo.ContainsKey("TotalNumber") && representationMsInfo.ContainsKey("SegmentDuration")) { segmentDuration = DoubleOrNull(representationMsInfo["SegmentDuration"], representationMsInfo["Timescale"]); representationMsInfo["TotalNumber"] = (int)Math.Ceiling(periodDuration.TotalSeconds / segmentDuration); } var fragments = new List>(); for (int i = representationMsInfo["StartNumber"]; i < representationMsInfo["StartNumber"] + representationMsInfo["TotalNumber"]; i++) { var segUrl = ""; if (mediaTemplate.Contains("{0:D")) { if (mediaTemplate.Contains("{{Bandwidth}}")) segUrl = PadNumber(mediaTemplate, "Bandwidth", bandwidth); if (mediaTemplate.Contains("{{Number}}")) segUrl = PadNumber(mediaTemplate, "Number", i); } else { segUrl = mediaTemplate.Replace("{{Bandwidth}}", bandwidth.ToString()); segUrl = segUrl.Replace("{{Number}}", i.ToString()); } fragments.Add(new Dictionary() { [mediaLocationKey] = CombineURL(baseUrl, segUrl), ["duration"] = segmentDuration }); } representationMsInfo["Fragments"] = fragments; } else { var fragments = new List>(); var segmentTime = 0L; var segmentD = 0L; var segmentNumber = representationMsInfo["StartNumber"]; void addSegmentUrl() { var segUrl = ""; if (mediaTemplate.Contains("{0:D")) { if (mediaTemplate.Contains("{{Bandwidth}}")) segUrl = PadNumber(mediaTemplate, "Bandwidth", bandwidth); if (mediaTemplate.Contains("{{Number}}")) segUrl = PadNumber(mediaTemplate, "Number", segmentNumber); if (mediaTemplate.Contains("{{Time}}")) segUrl = PadNumber(mediaTemplate, "Time", segmentTime); } else { segUrl = mediaTemplate.Replace("{{Bandwidth}}", bandwidth.ToString()); segUrl = segUrl.Replace("{{Number}}", segmentNumber.ToString()); segUrl = segUrl.Replace("{{Time}}", segmentTime.ToString()); } fragments.Add(new Dictionary() { [mediaLocationKey] = CombineURL(baseUrl, segUrl), ["duration"] = DoubleOrNull(segmentD, representationMsInfo["Timescale"]) }); } if (representationMsInfo.ContainsKey("S")) { for (int i = 0; i < representationMsInfo["S"].Count; i++) { var s = representationMsInfo["S"][i]; segmentTime = s["t"] == 0 ? segmentTime : s["t"]; segmentD = s["d"]; addSegmentUrl(); segmentNumber++; for (int j = 0; j < s["r"]; j++) { segmentTime += segmentD; addSegmentUrl(); segmentNumber++; } segmentTime += segmentD; } } representationMsInfo["Fragments"] = fragments; } } else if (representationMsInfo.ContainsKey("SegmentUrls") && representationMsInfo.ContainsKey("S")) { var fragments = new List>(); var segmentIndex = 0; var timescale = representationMsInfo["Timescale"]; foreach (var s in representationMsInfo["S"]) { var duration = DoubleOrNull(s["d"], timescale); for (int j = 0; j < s["r"] + 1; j++) { var segmentUri = representationMsInfo["SegmentUrls"][segmentIndex]; fragments.Add(new Dictionary() { [LocationKey(segmentUri)] = CombineURL(baseUrl, segmentUri), ["duration"] = duration }); segmentIndex++; } } representationMsInfo["Fragments"] = fragments; } else if (representationMsInfo.ContainsKey("SegmentUrls")) { var fragments = new List>(); var segmentDuration = DoubleOrNull(representationMsInfo["SegmentDuration"], representationMsInfo.ContainsKey("SegmentDuration") ? representationMsInfo["Timescale"] : 1); foreach (var segmentUrl in representationMsInfo["SegmentUrls"]) { if (segmentDuration != null) { fragments.Add(new Dictionary() { [LocationKey(segmentUrl)] = CombineURL(baseUrl, segmentUrl), ["duration"] = segmentDuration }); } else { fragments.Add(new Dictionary() { [LocationKey(segmentUrl)] = CombineURL(baseUrl, segmentUrl) }); } } representationMsInfo["Fragments"] = fragments; } if (representationMsInfo.ContainsKey("Fragments")) { f["Url"] = string.IsNullOrEmpty(mpdUrl) ? baseUrl : mpdUrl; f["FragmentBaseUrl"] = baseUrl; if (representationMsInfo.ContainsKey("InitializationUrl")) { f["InitializationUrl"] = CombineURL(baseUrl, representationMsInfo["InitializationUrl"]); if (f["InitializationUrl"].StartsWith("$$Range")) { f["InitializationUrl"] = CombineURL(baseUrl, f["InitializationUrl"]); } f["Fragments"] = representationMsInfo["Fragments"]; } } else { //整段mp4 f["Fragments"] = new List> { new Dictionary() { ["url"] = baseUrl, ["duration"] = ts.TotalSeconds } }; } //处理同一ID分散在不同Period的情况 if (formatList.Any(_f => _f["FormatId"] == f["FormatId"] && _f["Width"] == f["Width"] && _f["ContentType"] == f["ContentType"])) { for (int i = 0; i < formatList.Count; i++) { //参数相同但不在同一个Period才可以 if (formatList[i]["FormatId"] == f["FormatId"] && formatList[i]["Width"] == f["Width"] && formatList[i]["ContentType"] == f["ContentType"] && formatList[i]["PeriodIndex"] != f["PeriodIndex"]) { formatList[i]["Fragments"].AddRange(f["Fragments"]); break; } } } else { formatList.Add(f); } } } } } //排序 formatList.Sort((a, b) => { return (a["Width"] + a["Height"]) * 1000 + a["Tbr"] > (b["Width"] + b["Height"]) * 1000 + b["Tbr"] ? -1 : 1; }); //默认为最高码率的视频和音频 var bestVideo = SelectBestVideo(formatList); var bestAudio = SelectBestAudio(formatList); var audioLangList = new List(); formatList.ForEach(f => { if (f["ContentType"] == "audio" && !audioLangList.Contains(f["Language"])) audioLangList.Add(f["Language"]); }); if (audioLangList.Count > 1) { string Stringify(Dictionary f) { var type = f["ContentType"] == "audio" ? "Audio" : "Video"; var res = type == "Video" ? $"[{f["Width"]}x{f["Height"]}]" : ""; var id = $"[{f["FormatId"]}] "; var tbr = $"[{((int)f["Tbr"]).ToString().PadLeft(4)} Kbps] "; var asr = f["Asr"] != -1 ? $"[{f["Asr"]} Hz] " : ""; var fps = f["Fps"] != -1 ? $"[{f["Fps"]} fps] " : ""; var lang = string.IsNullOrEmpty(f["Language"]) ? "" : $"[{f["Language"]}] "; var codecs = $"[{f["Codecs"]}] "; return $"{type} => {id}{tbr}{asr}{fps}{lang}{codecs}{res}"; } var startCursorIndex = Console.CursorTop; var cursorIndex = startCursorIndex; for (int i = 0; i < formatList.Count; i++) { Console.WriteLine("".PadRight(13) + $"[{i.ToString().PadLeft(2)}]. {Stringify(formatList[i])}"); cursorIndex++; } LOGGER.PrintLine("Found Multiple Language Audio Tracks.\r\n" + "".PadRight(13) + "Please Select What You Want(Up to 1 Video and 1 Audio)."); Console.Write("".PadRight(13) + "Enter Numbers Separated By A Space: "); var input = Console.ReadLine(); cursorIndex += 2; try { for (int i = startCursorIndex; i < cursorIndex; i++) { Console.SetCursorPosition(0, i); Console.Write("".PadRight(300)); } Console.SetCursorPosition(0, startCursorIndex); } catch (Exception) { ; } if (!string.IsNullOrEmpty(input)) { bestVideo = new Dictionary() { ["Tbr"] = 0 }; bestAudio = new Dictionary() { ["Tbr"] = 0 }; foreach (var index in input.Split()) { var n = 0; int.TryParse(index, out n); if (formatList[n]["ContentType"] == "audio") { bestAudio = formatList[n]; } else { bestVideo = formatList[n]; } } } } if (bestVideo.Keys.Count > 1 && bestAudio.Keys.Count > 1) //音视频 { return GenerateMasterList(downDir, bestVideo, bestAudio); } else if (bestAudio.Keys.Count > 1) //仅有音频 { return GenerateMasterList(downDir, bestAudio); } else if (bestVideo.Keys.Count > 1) //仅有视频 { return GenerateMasterList(downDir, bestVideo); } else { return "ERROR"; } } /// /// 返回master文件地址 /// /// 存储目录 /// format列表 /// static string GenerateMasterList(string downDir, params Dictionary[] fs) { var audioPath = ""; var videoPath = ""; var bandwidth = 0; var codecsList = new List(); var res = ""; foreach (var f in fs) { var m3u8 = GenerateM3u8(f); bandwidth += Convert.ToInt32(f["Tbr"] * 1000); //Video if (m3u8.Contains("#EXT-VIDEO-WIDTH")) { var _path = Path.Combine(downDir, "mpdVideo.m3u8"); File.WriteAllText(_path, m3u8); videoPath = new Uri(_path).ToString(); res = f["Width"] + "x" + f["Height"]; } else { var _path = Path.Combine(downDir, "mpdAudio.m3u8"); File.WriteAllText(_path, m3u8); audioPath = new Uri(_path).ToString(); } codecsList.Add(f["Codecs"]); } var content = ""; if ((videoPath == "" && audioPath != "") || Global.VIDEO_TYPE == "IGNORE") { return audioPath; } else if (audioPath == "" && videoPath != "") { return videoPath; } else { if (!Directory.Exists(downDir + "(Audio)")) Directory.CreateDirectory(downDir + "(Audio)"); var _path = Path.Combine(downDir + "(Audio)", "mpdAudio.m3u8"); File.Copy(new Uri(audioPath).LocalPath, _path, true); audioPath = new Uri(_path).ToString(); content = $"#EXTM3U\r\n" + $"#EXT-X-MEDIA:TYPE=AUDIO,URI=\"{audioPath}\",GROUP-ID=\"default-audio-group\",NAME=\"stream_0\",AUTOSELECT=YES,CHANNELS=\"0\"\r\n" + $"#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},CODECS=\"{string.Join(",", codecsList)}\",RESOLUTION={res},AUDIO=\"default-audio-group\"\r\n" + $"{videoPath}"; } var _masterPath = Path.Combine(downDir, "master.m3u8"); File.WriteAllText(_masterPath, content); return new Uri(_masterPath).ToString(); } static string GenerateM3u8(Dictionary f) { StringBuilder sb = new StringBuilder(); sb.AppendLine("#EXTM3U"); sb.AppendLine("#EXT-X-VERSION:3"); sb.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); sb.AppendLine("#CREATED-BY:N_m3u8DL-CLI"); //Video if (f["ContentType"] != "audio") { sb.AppendLine($"#EXT-VIDEO-WIDTH:{f["Width"]}"); sb.AppendLine($"#EXT-VIDEO-HEIGHT:{f["Height"]}"); } sb.AppendLine($"#EXT-CODEC:{f["Codecs"]}"); sb.AppendLine($"#EXT-TBR:{f["Tbr"]}"); if (f.ContainsKey("InitializationUrl")) { string initUrl = f["InitializationUrl"]; if (MPD_URL.Contains("?") && MPD_URL.Contains(".kakao.com/")) { initUrl += new Regex("\\?.*").Match(MPD_URL).Value; } if (Regex.IsMatch(initUrl, "\\$\\$Range=(\\d+)-(\\d+)")) { var match = Regex.Match(initUrl, "\\$\\$Range=(\\d+)-(\\d+)"); string rangeStr = match.Value; long start = Convert.ToInt64(match.Groups[1].Value); long end = Convert.ToInt64(match.Groups[2].Value); sb.AppendLine($"#EXT-X-MAP:URI=\"{initUrl.Replace(rangeStr, "")}\",BYTERANGE=\"{end + 1 - start}@{start}\""); } else { sb.AppendLine($"#EXT-X-MAP:URI=\"{initUrl}\""); } } sb.AppendLine("#EXT-X-KEY:METHOD=PLZ-KEEP-RAW,URI=\"None\""); //使下载器使用二进制合并 List> fragments = f["Fragments"]; //检测最后一片的有效性 if (fragments.Count > 1) { bool checkValid(string url) { try { HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(new Uri(url)); request.Timeout = 120000; HttpWebResponse response = (HttpWebResponse)request.GetResponse(); if (((int)response.StatusCode).ToString().StartsWith("2")) return true; else return false; } catch (Exception) { return false; } } var last = fragments.Last(); var secondToLast = fragments[fragments.Count - 2]; var urlLast = last.ContainsKey("url") ? last["url"] : last["path"]; var urlSecondToLast = secondToLast.ContainsKey("url") ? secondToLast["url"] : secondToLast["path"]; if (MPD_URL.Contains("?") && MPD_URL.Contains(".kakao.com/")) { urlLast += new Regex("\\?.*").Match(MPD_URL).Value; urlSecondToLast += new Regex("\\?.*").Match(MPD_URL).Value; } //普通分段才判断 if (urlLast.StartsWith("http") && !Regex.IsMatch(urlLast, "\\$\\$Range=(\\d+)-(\\d+)")) { LOGGER.PrintLine(strings.checkingLast + (f["ContentType"] != "audio" ? "(Video)" : "(Audio)")); LOGGER.WriteLine(strings.checkingLast + (f["ContentType"] != "audio" ? "(Video)" : "(Audio)")); //倒数第二段正常,倒数第一段不正常 if (checkValid(urlSecondToLast) && !checkValid(urlLast)) fragments.RemoveAt(fragments.Count - 1); } } //添加分段 foreach (var seg in fragments) { var dur = seg.ContainsKey("duration") ? seg["duration"] : 0.0; var url = seg.ContainsKey("url") ? seg["url"] : seg["path"]; if (MPD_URL.Contains("?") && MPD_URL.Contains(".kakao.com/")) { url += new Regex("\\?.*").Match(MPD_URL).Value; } sb.AppendLine($"#EXTINF:{dur.ToString("0.00")}"); if (Regex.IsMatch(url, "\\$\\$Range=(\\d+)-(\\d+)")) { var match = Regex.Match(url, "\\$\\$Range=(\\d+)-(\\d+)"); string rangeStr = match.Value; long start = Convert.ToInt64(match.Groups[1].Value); long end = Convert.ToInt64(match.Groups[2].Value); sb.AppendLine($"#EXT-X-BYTERANGE:{end + 1 - start}@{start}"); sb.AppendLine(url.Replace(rangeStr, "")); } else { sb.AppendLine(url); } } sb.AppendLine("#EXT-X-ENDLIST"); return sb.ToString(); } static Dictionary SelectBestVideo(List> fs) { var best = new Dictionary() { ["Tbr"] = 0 }; var bandwidth = best["Tbr"]; var width = 0; foreach (var f in fs) { var w = f["Width"]; var h = f["Height"]; if (f["ContentType"] == "video") { if (f["Tbr"] > bandwidth && w > width) { best = f; bandwidth = f["Tbr"]; width = w; } } } return best; } static Dictionary SelectBestAudio(List> fs) { var best = new Dictionary() { ["Tbr"] = 0 }; var bandwidth = best["Tbr"]; foreach (var f in fs) { if (f["ContentType"] == "audio") { if (f["Tbr"] > bandwidth) { best = f; bandwidth = f["Tbr"]; } } } return best; } static int IntOrNull(object text, int scale = 1) { try { return Convert.ToInt32(text) / scale; } catch (Exception) { return -1; } } static double DoubleOrNull(object text, int scale = 1) { try { return Convert.ToDouble(text) / scale; } catch (Exception) { return -1; } } /// /// 拼接Baseurl和RelativeUrl /// /// Baseurl /// RelativeUrl /// static string CombineURL(string baseurl, string url) { if (url.StartsWith("$$Range")) { return baseurl + url; } Uri uri1 = new Uri(baseurl); Uri uri2 = new Uri(uri1, url); url = uri2.ToString(); return url; } } } ================================================ FILE: N_m3u8DL-CLI/MyOptions.cs ================================================ using CommandLine; using CommandLine.Text; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace N_m3u8DL_CLI { internal class MyOptions { [Value(0, Hidden = true, MetaName = "Input Source", HelpText = "Help_input", ResourceType = typeof(strings))] public string Input { get; set; } [Option("workDir", HelpText = "Help_workDir", ResourceType = typeof(strings))] public string WorkDir { get; set; } [Option("saveName", HelpText = "Help_saveName", ResourceType = typeof(strings))] public string SaveName { get; set; } = ""; [Option("baseUrl", HelpText = "Help_baseUrl", ResourceType = typeof(strings))] public string BaseUrl { get; set; } [Option("headers", HelpText = "Help_headers", ResourceType = typeof(strings))] public string Headers { get; set; } = ""; [Option("maxThreads", Default = 32U, HelpText = "Help_maxThreads", ResourceType = typeof(strings))] public uint MaxThreads { get; set; } [Option("minThreads", Default = 16U, HelpText = "Help_minThreads", ResourceType = typeof(strings))] public uint MinThreads { get; set; } [Option("retryCount", Default = 15U, HelpText = "Help_retryCount", ResourceType = typeof(strings))] public uint RetryCount { get; set; } [Option("timeOut", Default = 10U, HelpText = "Help_timeOut", ResourceType = typeof(strings))] public uint TimeOut { get; set; } [Option("muxSetJson", HelpText = "Help_muxSetJson", ResourceType = typeof(strings))] public string MuxSetJson { get; set; } [Option("useKeyFile", HelpText = "Help_useKeyFile", ResourceType = typeof(strings))] public string UseKeyFile { get; set; } [Option("useKeyBase64", HelpText = "Help_useKeyBase64", ResourceType = typeof(strings))] public string UseKeyBase64 { get; set; } [Option("useKeyIV", HelpText = "Help_useKeyIV", ResourceType = typeof(strings))] public string UseKeyIV { get; set; } [Option("downloadRange", HelpText = "Help_downloadRange", ResourceType = typeof(strings))] public string DownloadRange { get; set; } [Option("liveRecDur", HelpText = "Help_liveRecDur", ResourceType = typeof(strings))] public string LiveRecDur { get; set; } [Option("stopSpeed", HelpText = "Help_stopSpeed", ResourceType = typeof(strings))] public long StopSpeed { get; set; } = 0L; [Option("maxSpeed", HelpText = "Help_maxSpeed", ResourceType = typeof(strings))] public long MaxSpeed { get; set; } = 0L; [Option("proxyAddress", HelpText = "Help_proxyAddress", ResourceType = typeof(strings))] public string ProxyAddress { get; set; } [Option("enableDelAfterDone", HelpText = "Help_enableDelAfterDone", ResourceType = typeof(strings))] public bool EnableDelAfterDone { get; set; } [Option("enableMuxFastStart", HelpText = "Help_enableMuxFastStart", ResourceType = typeof(strings))] public bool EnableMuxFastStart { get; set; } [Option("enableBinaryMerge", HelpText = "Help_enableBinaryMerge", ResourceType = typeof(strings))] public bool EnableBinaryMerge { get; set; } [Option("enableParseOnly", HelpText = "Help_enableParseOnly", ResourceType = typeof(strings))] public bool EnableParseOnly { get; set; } [Option("enableAudioOnly", HelpText = "Help_enableAudioOnly", ResourceType = typeof(strings))] public bool EnableAudioOnly { get; set; } [Option("disableDateInfo", HelpText = "Help_disableDateInfo", ResourceType = typeof(strings))] public bool DisableDateInfo { get; set; } [Option("disableIntegrityCheck", HelpText = "Help_disableIntegrityCheck", ResourceType = typeof(strings))] public bool DisableIntegrityCheck { get; set; } [Option("noMerge", HelpText = "Help_noMerge", ResourceType = typeof(strings))] public bool NoMerge { get; set; } [Option("noProxy", HelpText = "Help_noProxy", ResourceType = typeof(strings))] public bool NoProxy { get; set; } [Option("registerUrlProtocol", HelpText = "Help_registerUrlProtocol", ResourceType = typeof(strings))] public bool RegisterUrlProtocol { get; set; } [Option("unregisterUrlProtocol", HelpText = "Help_unregisterUrlProtocol", ResourceType = typeof(strings))] public bool UnregisterUrlProtocol { get; set; } [Option("enableChaCha20", HelpText = "enableChaCha20")] public bool EnableChaCha20 { get; set; } [Option("chaCha20KeyBase64", HelpText = "ChaCha20KeyBase64")] public string ChaCha20KeyBase64 { get; set; } [Option("chaCha20NonceBase64", HelpText = "ChaCha20NonceBase64")] public string ChaCha20NonceBase64 { get; set; } } } ================================================ FILE: N_m3u8DL-CLI/N_m3u8DL-CLI.csproj ================================================  Debug AnyCPU {4FB61439-B738-46AC-B3AF-2BF72150D057} Exe N_m3u8DL_CLI N_m3u8DL-CLI v4.6 512 true x86 true full false bin\Debug\ DEBUG;TRACE prompt 4 false AnyCPU pdbonly true bin\Release\ TRACE prompt 4 false logo_3Iv_icon.ico ..\packages\BrotliSharpLib.0.3.3\lib\net451\BrotliSharpLib.dll ..\packages\CommandLineParser.2.8.0\lib\net45\CommandLine.dll ..\packages\Costura.Fody.4.1.0\lib\net40\Costura.dll ..\packages\TaskScheduler.2.8.7\lib\net452\Microsoft.Win32.TaskScheduler.dll ..\packages\HttpToSocks5Proxy.1.4.0\lib\net45\MihaZupan.HttpToSocks5Proxy.dll ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll ..\packages\NiL.JS.2.5.1428\lib\net45\NiL.JS.dll ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll ..\packages\UACHelper.1.3.0.5\lib\net40\UACHelper.dll True True strings.resx True True strings.en-US.resx True True strings.zh-TW.resx Designer {420B2830-E718-11CF-893D-00A0C9054228} 1 0 0 tlbimp False True PublicResXFileCodeGenerator strings.en-US.Designer.cs Designer PublicResXFileCodeGenerator strings.Designer.cs Designer PublicResXFileCodeGenerator strings.zh-TW.Designer.cs Designer 这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。 ================================================ FILE: N_m3u8DL-CLI/Parser.cs ================================================ using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Text.RegularExpressions; namespace N_m3u8DL_CLI { class Parser { struct Audio { public string Name; public string Language; public string Uri; public string Channels; public override string ToString() { return $"[{Name}] [{Language}] [{(string.IsNullOrEmpty(Channels) ? "" : $"{Channels}ch")}]".Replace("[]", ""); } } struct Subtitle { public string Name; public string Language; public string Uri; public override string ToString() { return $"[{Name}] [{Language}]"; } } //存储上一行key的信息,如果一样,就跳过下载key这一步 private string lastKeyLine = string.Empty; //METHOD, KEY, IV string[] m3u8CurrentKey = new string[] { "NONE", "", "" }; private string m3u8SavePath = string.Empty; private string jsonSavePath = string.Empty; private long bestBandwidth = 0; private string bestUrl = string.Empty; private string bestUrlAudio = string.Empty; private string bestUrlSub = string.Empty; Dictionary> MEDIA_AUDIO_GROUP = new Dictionary>(); //外挂音频所有分组信息 private string audioUrl = string.Empty; //音轨地址 Dictionary> MEDIA_SUB_GROUP = new Dictionary>(); //外挂字幕所有分组信息 private string subUrl = string.Empty; //字幕地址 //存放多轨道的信息 private ArrayList extLists = new ArrayList(); //标记是否已清除优酷广告分片 private static bool hasAd = false; public string BaseUrl { get; set; } = string.Empty; public string M3u8Url { get; set; } = string.Empty; public string DownDir { get; set; } = string.Empty; public string DownName { get; set; } = string.Empty; public string Headers { get; set; } = string.Empty; //存放Range信息,允许用户只下载部分视频 public static int RangeStart { get; set; } = 0; public static int RangeEnd { get; set; } = -1; //是否自动清除优酷广告分片 public static bool DelAd { get; set; } = true; //存放Range信息,允许用户只下载部分视频 public static string DurStart { get; set; } = ""; public static string DurEnd { get; set; } = ""; public string KeyFile { get; set; } = string.Empty; public string KeyBase64 { get; set; } = string.Empty; public bool LiveStream { get; set; } = false; public string KeyIV { get; set; } = string.Empty; public void Parse() { FFmpeg.REC_TIME = ""; m3u8SavePath = Path.Combine(DownDir, "raw.m3u8"); jsonSavePath = Path.Combine(DownDir, "meta.json"); if (!Directory.Exists(DownDir))//若文件夹不存在则新建文件夹 Directory.CreateDirectory(DownDir); //新建文件夹 //存放分部的所有信息(#EXT-X-DISCONTINUITY) JArray parts = new JArray(); //存放分片的所有信息 JArray segments = new JArray(); JObject segInfo = new JObject(); extLists.Clear(); MEDIA_AUDIO_GROUP.Clear(); MEDIA_SUB_GROUP.Clear(); string m3u8Content = string.Empty; string m3u8Method = string.Empty; string[] extMAP = { "", "" }; string[] extList = new string[10]; long segIndex = 0; long startIndex = 0; int targetDuration = 0; double totalDuration = 0; bool expectSegment = false, expectPlaylist = false, isIFramesOnly = false, isIndependentSegments = false, isEndlist = false, isAd = false, isM3u = false; //获取m3u8内容 //if (!LiveStream) // LOGGER.PrintLine(strings.downloadingM3u8, LOGGER.Warning); //if (M3u8Url.Contains(".cntv.")) //{ // M3u8Url = M3u8Url.Replace("/h5e/", "/"); //} if (M3u8Url.StartsWith("http")) { if (M3u8Url.Contains("nfmovies.com/hls")) m3u8Content = DecodeNfmovies.DecryptM3u8(Global.HttpDownloadFileToBytes(M3u8Url, Headers)); else if (M3u8Url.Contains("hls.ddyunp.com/ddyun") || M3u8Url.Contains("hls.90mm.me/ddyun")) m3u8Content = DecodeDdyun.DecryptM3u8(Global.HttpDownloadFileToBytes(DecodeDdyun.GetVaildM3u8Url(M3u8Url), Headers)); else m3u8Content = Global.GetWebSource(M3u8Url, Headers); } else if (M3u8Url.StartsWith("file:")) { Uri t = new Uri(M3u8Url); m3u8Content = File.ReadAllText(t.LocalPath); } else if (File.Exists(M3u8Url)) { m3u8Content = File.ReadAllText(M3u8Url); if (!M3u8Url.Contains("\\")) M3u8Url = Path.Combine(Environment.CurrentDirectory, M3u8Url); Uri t = new Uri(M3u8Url); M3u8Url = t.ToString(); } if (m3u8Content == "") return; if (M3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && M3u8Url.Contains("endtime=")) isEndlist = true; if (M3u8Url.Contains("imooc.com/")) { m3u8Content = DecodeImooc.DecodeM3u8(m3u8Content); } if (m3u8Content.Contains("") && m3u8Content.Contains("[@] long expectByte = -1; //parm n long startByte = 0; //parm o while ((line = sr.ReadLine()) != null) { if (string.IsNullOrEmpty(line)) continue; if (line.StartsWith(HLSTags.ext_m3u)) isM3u = true; //只下载部分字节 else if (line.StartsWith(HLSTags.ext_x_byterange)) { string[] t = line.Replace(HLSTags.ext_x_byterange + ":", "").Split('@'); if (t.Length > 0) { if (t.Length == 1) { expectByte = Convert.ToInt64(t[0]); segInfo.Add("expectByte", expectByte); segInfo.Add("startByte", segments.Last["startByte"].Value() + segments.Last["expectByte"].Value()); } if (t.Length == 2) { expectByte = Convert.ToInt64(t[0]); startByte = Convert.ToInt64(t[1]); segInfo.Add("expectByte", expectByte); segInfo.Add("startByte", startByte); } } expectSegment = true; } //国家地理去广告 else if (line.StartsWith("#UPLYNK-SEGMENT")) { if (line.Contains(",ad")) isAd = true; else if (line.Contains(",segment")) isAd = false; } //国家地理去广告 else if (isAd) { continue; } //解析定义的分段长度 else if (line.StartsWith(HLSTags.ext_x_targetduration)) { targetDuration = Convert.ToInt32(Convert.ToDouble(line.Replace(HLSTags.ext_x_targetduration + ":", "").Trim())); } //解析起始编号 else if (line.StartsWith(HLSTags.ext_x_media_sequence)) { segIndex = Convert.ToInt64(line.Replace(HLSTags.ext_x_media_sequence + ":", "").Trim()); startIndex = segIndex; } else if (line.StartsWith(HLSTags.ext_x_discontinuity_sequence)) ; else if (line.StartsWith(HLSTags.ext_x_program_date_time)) { if (string.IsNullOrEmpty(FFmpeg.REC_TIME)) { FFmpeg.REC_TIME = line.Replace(HLSTags.ext_x_program_date_time + ":", "").Trim(); } } //解析不连续标记,需要单独合并(timestamp不同) else if (line.StartsWith(HLSTags.ext_x_discontinuity)) { //修复优酷去除广告后的遗留问题 if (hasAd && parts.Count > 0) { segments = (JArray)parts[parts.Count - 1]; parts.RemoveAt(parts.Count - 1); hasAd = false; continue; } //常规情况的#EXT-X-DISCONTINUITY标记,新建part if (!hasAd && segments.Count >= 1) { parts.Add(segments); segments = new JArray(); } } else if (line.StartsWith(HLSTags.ext_x_cue_out)) ; else if (line.StartsWith(HLSTags.ext_x_cue_out_start)) ; else if (line.StartsWith(HLSTags.ext_x_cue_span)) ; else if (line.StartsWith(HLSTags.ext_x_version)) ; else if (line.StartsWith(HLSTags.ext_x_allow_cache)) ; //解析KEY else if (line.StartsWith(HLSTags.ext_x_key)) { //自定义KEY情况 判断是否需要读取IV if (!string.IsNullOrEmpty(KeyFile) || !string.IsNullOrEmpty(KeyBase64)) { if (m3u8CurrentKey[2] == "" && line.Contains("IV=0x")) { var temp = Global.GetTagAttribute(line.Replace(HLSTags.ext_x_key + ":", ""), "IV"); m3u8CurrentKey[2] = temp; //使用m3u8中的IV } } else { m3u8CurrentKey = ParseKey(line); //存储为上一行的key信息 lastKeyLine = line; } } //解析分片时长(暂时不考虑标题属性) else if (line.StartsWith(HLSTags.extinf)) { string[] tmp = line.Replace(HLSTags.extinf + ":", "").Split(','); segDuration = Convert.ToDouble(tmp[0]); segInfo["index"] = segIndex; segInfo["method"] = m3u8CurrentKey[0]; //是否有加密,有的话写入KEY和IV if (m3u8CurrentKey[0] != "NONE") { segInfo["key"] = m3u8CurrentKey[1]; //没有读取到IV,自己生成 if (m3u8CurrentKey[2] == "") segInfo["iv"] = "0x" + Convert.ToString(segIndex, 16).PadLeft(32, '0'); else segInfo["iv"] = m3u8CurrentKey[2]; } totalDuration += segDuration; segInfo["duration"] = segDuration; expectSegment = true; segIndex++; } //解析STREAM属性 else if (line.StartsWith(HLSTags.ext_x_stream_inf)) { expectPlaylist = true; string bandwidth = Global.GetTagAttribute(line, "BANDWIDTH"); string average_bandwidth = Global.GetTagAttribute(line, "AVERAGE-BANDWIDTH"); string codecs = Global.GetTagAttribute(line, "CODECS"); string resolution = Global.GetTagAttribute(line, "RESOLUTION"); string frame_rate = Global.GetTagAttribute(line, "FRAME-RATE"); string hdcp_level = Global.GetTagAttribute(line, "HDCP-LEVEL"); string audio = Global.GetTagAttribute(line, "AUDIO"); string video = Global.GetTagAttribute(line, "VIDEO"); string subtitles = Global.GetTagAttribute(line, "SUBTITLES"); string closed_captions = Global.GetTagAttribute(line, "CLOSED-CAPTIONS"); extList = new string[] { bandwidth, average_bandwidth, codecs, resolution, frame_rate,hdcp_level,audio,video,subtitles,closed_captions }; } else if (line.StartsWith(HLSTags.ext_x_i_frame_stream_inf)) ; else if (line.StartsWith(HLSTags.ext_x_media)) { var groupId = Global.GetTagAttribute(line, "GROUP-ID"); if (Global.GetTagAttribute(line, "TYPE") == "AUDIO") { var audio = new Audio(); audio.Channels = Global.GetTagAttribute(line, "CHANNELS"); audio.Language = Global.GetTagAttribute(line, "LANGUAGE"); audio.Name = Global.GetTagAttribute(line, "NAME"); audio.Uri = CombineURL(BaseUrl, Global.GetTagAttribute(line, "URI")); if (!MEDIA_AUDIO_GROUP.ContainsKey(groupId)) { MEDIA_AUDIO_GROUP.Add(groupId, new List