├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── build_latest.yml ├── .gitignore ├── LICENSE ├── N_m3u8DL-CLI.sln ├── 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 ├── logo_3Iv_icon.ico ├── 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 ├── README.md ├── README_ENG.md └── docs ├── Advanced.html ├── GetM3u8.html ├── Introductory.html ├── M3U8URL2File.html ├── SimpleGUI.html ├── gitbook ├── fonts │ └── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── gitbook-plugin-donate │ ├── plugin.css │ └── plugin.js ├── gitbook-plugin-fontsettings │ ├── fontsettings.js │ └── website.css ├── gitbook-plugin-github-buttons │ ├── plugin.js │ └── plugin.js.map ├── gitbook-plugin-github │ └── plugin.js ├── gitbook-plugin-highlight │ ├── ebook.css │ └── website.css ├── gitbook-plugin-lunr │ ├── lunr.min.js │ └── search-lunr.js ├── gitbook-plugin-search │ ├── lunr.min.js │ ├── search-engine.js │ ├── search.css │ └── search.js ├── gitbook-plugin-sharing-plus │ └── buttons.js ├── gitbook.js ├── images │ ├── apple-touch-icon-precomposed-152.png │ └── favicon.ico ├── style.css └── theme.js ├── index.html ├── search_index.json └── source └── images ├── GUI.png ├── M3U8URL2File.gif ├── alipay.png ├── muxSetJson.png └── 直接使用.gif /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://nilaoda.github.io/N_m3u8DL-CLI/source/images/alipay.png','https://www.buymeacoffee.com/nilaoda'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build_latest.yml: -------------------------------------------------------------------------------- 1 | name: Build_Latest 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | name: Checkout Code 13 | 14 | - name: Setup MSBuild Path 15 | uses: warrenbuckley/Setup-MSBuild@v1 16 | env: 17 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 18 | 19 | - name: Setup NuGet 20 | uses: NuGet/setup-nuget@v1.0.2 21 | env: 22 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 23 | 24 | - name: Restore NuGet Packages 25 | run: nuget restore N_m3u8DL-CLI.sln 26 | 27 | - name: Build 28 | run: msbuild N_m3u8DL-CLI.sln /p:Configuration=Release /p:DebugSymbols=false /p:DebugType=None 29 | 30 | - name: Upload Artifact 31 | uses: actions/upload-artifact@v1.0.0 32 | with: 33 | name: N_m3u8DL-CLI_latest 34 | path: N_m3u8DL-CLI\bin\Release\N_m3u8DL-CLI.exe 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 nilaoda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29215.179 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N_m3u8DL-CLI", "N_m3u8DL-CLI\N_m3u8DL-CLI.csproj", "{4FB61439-B738-46AC-B3AF-2BF72150D057}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4FB61439-B738-46AC-B3AF-2BF72150D057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4FB61439-B738-46AC-B3AF-2BF72150D057}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4FB61439-B738-46AC-B3AF-2BF72150D057}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4FB61439-B738-46AC-B3AF-2BF72150D057}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {82B9270D-B7B2-4591-BF8A-5B4EBCD0EA8A} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/DecodeCdeledu.cs: -------------------------------------------------------------------------------- 1 | using NiL.JS.BaseLibrary; 2 | using NiL.JS.Core; 3 | using NiL.JS.Extensions; 4 | using System; 5 | using Array = System.Array; 6 | 7 | namespace N_m3u8DL_CLI 8 | { 9 | internal class DecodeCdeledu 10 | { 11 | private static string JS = @" 12 | var _keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 13 | 14 | var removePaddingChars = function(input) { 15 | var lkey = _keyStr.indexOf(input.charAt(input.length - 1)); 16 | if (lkey == 64) { 17 | return input.substring(0, input.length - 1); 18 | } 19 | return input; 20 | } 21 | 22 | var base64Decode = function(input, arrayBuffer) { 23 | input = removePaddingChars(input); 24 | input = removePaddingChars(input); 25 | var bytes = parseInt((input.length / 4) * 3, 10); 26 | var uarray; 27 | var chr1, chr2, chr3; 28 | var enc1, enc2, enc3, enc4; 29 | var i = 0; 30 | var j = 0; 31 | if (arrayBuffer) { 32 | uarray = new Uint8Array(arrayBuffer); 33 | } else { 34 | uarray = new Uint8Array(bytes); 35 | } 36 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); 37 | for (i = 0; i < bytes; i += 3) { 38 | enc1 = _keyStr.indexOf(input.charAt(j++)); 39 | enc2 = _keyStr.indexOf(input.charAt(j++)); 40 | enc3 = _keyStr.indexOf(input.charAt(j++)); 41 | enc4 = _keyStr.indexOf(input.charAt(j++)); 42 | chr1 = (enc1 << 2) | (enc2 >> 4); 43 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 44 | chr3 = ((enc3 & 3) << 6) | enc4; 45 | uarray[i] = chr1; 46 | if (enc3 != 64) 47 | uarray[i + 1] = chr2; 48 | if (enc4 != 64) 49 | uarray[i + 2] = chr3; 50 | } 51 | return uarray; 52 | } 53 | 54 | var uint8ArrayToString = function(uDataArr) { 55 | var arrStr = ''; 56 | for (var i = 0; i < uDataArr.length; i++) { 57 | arrStr += String.fromCharCode(uDataArr[i]); 58 | } 59 | return arrStr; 60 | } 61 | 62 | var decodeKey = function(dataKeyString) { 63 | var decodeArr = base64Decode(dataKeyString); 64 | var decodeArrString = uint8ArrayToString(decodeArr); 65 | return decodeArrString; 66 | if (decodeArrString.indexOf('|&|') > 0) { 67 | return decodeArrString; 68 | } 69 | return ''; 70 | } 71 | "; 72 | //https://video.cdeledu.com/js/lib/cdel.hls.min-1.0.js?v=1.3 73 | public static string DecodeKey(string txt) 74 | { 75 | var context = new Context(); 76 | context.Eval(JS); 77 | var concatFunction = context.GetVariable("decodeKey").As(); 78 | string key = concatFunction.Call(new Arguments { txt }).ToString(); 79 | string realKey = key.Split(new string[] { "|&|" }, StringSplitOptions.None)[1]; 80 | return realKey; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/DecodeDdyun.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace N_m3u8DL_CLI 6 | { 7 | class DecodeDdyun 8 | { 9 | public static string DecryptM3u8(byte[] byteArray) 10 | { 11 | string tmp = DecodeNfmovies.DecryptM3u8(byteArray); 12 | if (tmp.StartsWith("duoduo.key")) 13 | { 14 | tmp = Regex.Replace(tmp, @"#EXT-X-BYTERANGE:.*\s", ""); 15 | tmp = tmp.Replace("https:", "jump/https:") 16 | .Replace("inews.gtimg.com", "puui.qpic.cn"); 17 | } 18 | return tmp; 19 | } 20 | 21 | //https://player.ddyunp.com/jQuery.min.js?v1.5 22 | public static string GetVaildM3u8Url(string url) 23 | { 24 | //url: https://hls.ddyunp.com/ddyun/id/1/key/playlist.m3u8 25 | string id = Regex.Match(url, @"\w{20,}").Value; 26 | string tm = Global.GetTimeStamp(false); 27 | string t = ((long.Parse(tm) / 0x186a0) * 0x64).ToString(); 28 | string tmp = id + "duoduo" + "1" + t; 29 | MD5 md5 = MD5.Create(); 30 | byte[] bs = Encoding.UTF8.GetBytes(tmp); 31 | byte[] hs = md5.ComputeHash(bs); 32 | StringBuilder sb = new StringBuilder(); 33 | foreach (byte b in hs) 34 | { 35 | sb.Append(b.ToString("x2")); 36 | } 37 | string key = sb.ToString(); 38 | return Regex.Replace(url, @"1/\w{20,}", "1/" + key); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/DecodeHuke88Key.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace N_m3u8DL_CLI 9 | { 10 | //https://js.huke88.com/assets/revision/js/plugins/tcplayer/tcplayer.v4.1.min.js?v=930 11 | //https://js.huke88.com/assets/revision/js/plugins/tcplayer/libs/hls.min.0.13.2m.js?v=930 12 | class DecodeHuke88Key 13 | { 14 | private static string[] GetOverlayInfo(string url) 15 | { 16 | var enc = new Regex("eyJ\\w{100,}").Match(url).Value; 17 | var json = Encoding.UTF8.GetString(Convert.FromBase64String(enc)); 18 | JObject jObject = JObject.Parse(json); 19 | var key = jObject["overlayKey"].ToString(); 20 | var iv = jObject["overlayIv"].ToString(); 21 | return new string[] { key, iv }; 22 | } 23 | 24 | public static string DecodeKey(string url, byte[] data) 25 | { 26 | var info = GetOverlayInfo(url); 27 | var overlayKey = info[0]; 28 | var overlayIv = info[1]; 29 | var l = new List(); 30 | var c = new List(); 31 | for (int h = 0; h < 16; h++) 32 | { 33 | var f = overlayKey.Substring(2 * h, 2); 34 | var g = overlayIv.Substring(2 * h, 2); 35 | l.Add(Convert.ToByte(f, 16)); 36 | c.Add(Convert.ToByte(g, 16)); 37 | } 38 | 39 | var _lastCipherblock = c.ToArray(); 40 | 41 | var t = new byte[data.Length]; 42 | var r = data; 43 | r = Decrypter.AES128Decrypt(data, l.ToArray(), Decrypter.HexStringToBytes("00000000000000000000000000000000"), CipherMode.CBC, PaddingMode.Zeros); 44 | 45 | for (var o = 0; o < 16; o++) 46 | t[o] = (byte)(r[o] ^ _lastCipherblock[o]); 47 | 48 | var key = Convert.ToBase64String(t); 49 | 50 | return key; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/DecodeImooc.cs: -------------------------------------------------------------------------------- 1 | using NiL.JS.BaseLibrary; 2 | using NiL.JS.Core; 3 | using NiL.JS.Extensions; 4 | using System; 5 | using Array = System.Array; 6 | 7 | namespace N_m3u8DL_CLI 8 | { 9 | /* 10 | * js代码来自:https://www.imooc.com/static/moco/player/3.0.6.3/mocoplayer.js?v=202006122046 11 | * 12 | */ 13 | class DecodeImooc 14 | { 15 | private static string JS = @" 16 | function n(t, e) { 17 | function r(t, e) { 18 | var r = ''; 19 | if ('object' == typeof t) 20 | for (var n = 0; n < t.length; n++) 21 | r += String.fromCharCode(t[n]); 22 | t = r || t; 23 | for (var i, o, a = new Uint8Array(t.length), s = e.length, n = 0; n < t.length; n++) 24 | o = n % s, 25 | i = t[n], 26 | i = i.toString().charCodeAt(0), 27 | a[n] = i ^ e.charCodeAt(o); 28 | return a 29 | } 30 | function n(t) { 31 | var e = ''; 32 | if ('object' == typeof t) 33 | for (var r = 0; r < t.length; r++) 34 | e += String.fromCharCode(t[r]); 35 | t = e || t; 36 | var n = new Uint8Array(t.length); 37 | for (r = 0; r < t.length; r++) 38 | n[r] = t[r].toString().charCodeAt(0); 39 | var i, o, r = 0; 40 | for (r = 0; r < n.length; r++) 41 | 0 != (i = n[r] % 3) && r + i < n.length && (o = n[r + 1], 42 | n[r + 1] = n[r + i], 43 | n[r + i] = o, 44 | r = r + i + 1); 45 | return n 46 | } 47 | function i(t) { 48 | var e = ''; 49 | if ('object' == typeof t) 50 | for (var r = 0; r < t.length; r++) 51 | e += String.fromCharCode(t[r]); 52 | t = e || t; 53 | var n = new Uint8Array(t.length); 54 | for (r = 0; r < t.length; r++) 55 | n[r] = t[r].toString().charCodeAt(0); 56 | var r = 0 57 | , i = 0 58 | , o = 0 59 | , a = 0; 60 | for (r = 0; r < n.length; r++) 61 | o = n[r] % 2, 62 | o && r++, 63 | a++; 64 | var s = new Uint8Array(a); 65 | for (r = 0; r < n.length; r++) 66 | o = n[r] % 2, 67 | s[i++] = o ? n[r++] : n[r]; 68 | return s 69 | } 70 | function o(t, e) { 71 | var r = 0 72 | , n = 0 73 | , i = 0 74 | , o = 0 75 | , a = ''; 76 | if ('object' == typeof t) 77 | for (var r = 0; r < t.length; r++) 78 | a += String.fromCharCode(t[r]); 79 | t = a || t; 80 | var s = new Uint8Array(t.length); 81 | for (r = 0; r < t.length; r++) 82 | s[r] = t[r].toString().charCodeAt(0); 83 | for (r = 0; r < t.length; r++) 84 | if (0 != (o = s[r] % 5) && 1 != o && r + o < s.length && (i = s[r + 1], 85 | n = r + 2, 86 | s[r + 1] = s[r + o], 87 | s[o + r] = i, 88 | (r = r + o + 1) - 2 > n)) 89 | for (; n < r - 2; n++) 90 | s[n] = s[n] ^ e.charCodeAt(n % e.length); 91 | for (r = 0; r < t.length; r++) 92 | s[r] = s[r] ^ e.charCodeAt(r % e.length); 93 | return s 94 | } 95 | for (var a = { 96 | data: { 97 | info: t 98 | } 99 | }, s = { 100 | q: r, 101 | h: n, 102 | m: i, 103 | k: o 104 | }, l = a.data.info, u = l.substring(l.length - 4).split(''), c = 0; c < u.length; c++) 105 | u[c] = u[c].toString().charCodeAt(0) % 4; 106 | u.reverse(); 107 | for (var d = [], c = 0; c < u.length; c++) 108 | d.push(l.substring(u[c] + 1, u[c] + 2)), 109 | l = l.substring(0, u[c] + 1) + l.substring(u[c] + 2); 110 | a.data.encrypt_table = d, 111 | a.data.key_table = []; 112 | for (var c in a.data.encrypt_table) 113 | 'q' != a.data.encrypt_table[c] && 'k' != a.data.encrypt_table[c] || (a.data.key_table.push(l.substring(l.length - 12)), 114 | l = l.substring(0, l.length - 12)); 115 | a.data.key_table.reverse(), 116 | a.data.info = l; 117 | 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); 118 | a.data.info = function(t) { 119 | var e, r, n, i, o, a, s; 120 | for (a = t.length, 121 | o = 0, 122 | s = ''; o < a; ) { 123 | do { 124 | e = f[255 & t.charCodeAt(o++)] 125 | } while (o < a && -1 == e);if (-1 == e) 126 | break; 127 | do { 128 | r = f[255 & t.charCodeAt(o++)] 129 | } while (o < a && -1 == r);if (-1 == r) 130 | break; 131 | s += String.fromCharCode(e << 2 | (48 & r) >> 4); 132 | do { 133 | if (61 == (n = 255 & t.charCodeAt(o++))) 134 | return s; 135 | n = f[n] 136 | } while (o < a && -1 == n);if (-1 == n) 137 | break; 138 | s += String.fromCharCode((15 & r) << 4 | (60 & n) >> 2); 139 | do { 140 | if (61 == (i = 255 & t.charCodeAt(o++))) 141 | return s; 142 | i = f[i] 143 | } while (o < a && -1 == i);if (-1 == i) 144 | break; 145 | s += String.fromCharCode((3 & n) << 6 | i) 146 | } 147 | return s 148 | }(a.data.info); 149 | for (var c in a.data.encrypt_table) { 150 | var h = a.data.encrypt_table[c]; 151 | if ('q' == h || 'k' == h) { 152 | var p = a.data.key_table.pop(); 153 | a.data.info = s[a.data.encrypt_table[c]](a.data.info, p) 154 | } else 155 | a.data.info = s[a.data.encrypt_table[c]](a.data.info) 156 | } 157 | if (e) 158 | return a.data.info; 159 | var g = ''; 160 | for (c = 0; c < a.data.info.length; c++) 161 | g += String.fromCharCode(a.data.info[c]); 162 | return g 163 | } 164 | function Uint8ArrayToString(fileData){ 165 | var dataString = ''; 166 | for (var i = 0; i < fileData.length; i++) { 167 | dataString += Number(fileData[i]) + ','; 168 | } 169 | return dataString; 170 | } 171 | function decodeKey(resp){ 172 | var string = eval('('+resp+')'); 173 | //return btoa(String.fromCharCode.apply(null, new Uint8Array(n(string.data.info, 1)))); 174 | return Uint8ArrayToString(new Uint8Array(n(string.data.info, 1))); 175 | } 176 | function decodeM3u8(resp){ 177 | var string = eval('('+resp+')'); 178 | return n(string.data.info); 179 | } 180 | "; 181 | 182 | 183 | public static string DecodeM3u8(string resp) 184 | { 185 | var context = new Context(); 186 | context.Eval(JS); 187 | var concatFunction = context.GetVariable("decodeM3u8").As(); 188 | string m3u8 = concatFunction.Call(new Arguments { resp }).ToString(); 189 | return m3u8; 190 | } 191 | 192 | public static string DecodeKey(string resp) 193 | { 194 | var context = new Context(); 195 | context.Eval(JS); 196 | var concatFunction = context.GetVariable("decodeKey").As(); 197 | string key = concatFunction.Call(new Arguments { resp }).ToString(); 198 | byte[] v = Array.ConvertAll(key.Trim(',').Split(','), s => (byte)int.Parse(s)); 199 | string realKey = Convert.ToBase64String(v); 200 | return realKey; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/DecodeNfmovies.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace N_m3u8DL_CLI 7 | { 8 | class DecodeNfmovies 9 | { 10 | //https://jx.nfmovies.com/hls.min.js 11 | public static string DecryptM3u8(byte[] byteArray) 12 | { 13 | var t = byteArray; 14 | var decrypt = ""; 15 | if (137 == t[0] && 80 == t[1] && 130 == t[354] && 96 == t[353]) t = t.Skip(355).ToArray(); 16 | else 17 | { 18 | if (137 != t[0] || 80 != t[1] || 130 != t[394] || 96 != t[393]) 19 | { 20 | for (var i = 0; i < t.Length; i++) decrypt += Convert.ToChar(t[i]); 21 | return decrypt; 22 | } 23 | t = t.Skip(395).ToArray(); 24 | } 25 | using (var zipStream = 26 | new System.IO.Compression.GZipStream(new MemoryStream(t), System.IO.Compression.CompressionMode.Decompress)) 27 | { 28 | using (StreamReader sr = new StreamReader(zipStream, Encoding.UTF8)) 29 | { 30 | decrypt = sr.ReadToEnd(); 31 | } 32 | } 33 | return decrypt; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/Decrypter.cs: -------------------------------------------------------------------------------- 1 | using CSChaCha20; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | 7 | namespace N_m3u8DL_CLI 8 | { 9 | class Decrypter 10 | { 11 | public static byte[] AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) 12 | { 13 | FileStream fs = new FileStream(filePath, FileMode.Open); 14 | //获取文件大小 15 | long size = fs.Length; 16 | byte[] inBuff = new byte[size]; 17 | fs.Read(inBuff, 0, inBuff.Length); 18 | fs.Close(); 19 | 20 | Aes dcpt = Aes.Create(); 21 | dcpt.BlockSize = 128; 22 | dcpt.KeySize = 128; 23 | dcpt.Key = keyByte; 24 | dcpt.IV = ivByte; 25 | dcpt.Mode = mode; 26 | dcpt.Padding = padding; 27 | 28 | ICryptoTransform cTransform = dcpt.CreateDecryptor(); 29 | Byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); 30 | return resultArray; 31 | } 32 | 33 | public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) 34 | { 35 | byte[] inBuff = encryptedBuff; 36 | 37 | Aes dcpt = Aes.Create(); 38 | dcpt.BlockSize = 128; 39 | dcpt.KeySize = 128; 40 | dcpt.Key = keyByte; 41 | dcpt.IV = ivByte; 42 | dcpt.Mode = mode; 43 | dcpt.Padding = padding; 44 | 45 | ICryptoTransform cTransform = dcpt.CreateDecryptor(); 46 | Byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); 47 | return resultArray; 48 | } 49 | 50 | public static byte[] CHACHA20Decrypt(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes) 51 | { 52 | if (keyBytes.Length != 32) 53 | throw new Exception("Key must be 32 bytes!"); 54 | if (nonceBytes.Length != 12 && nonceBytes.Length != 8) 55 | throw new Exception("Key must be 12 or 8 bytes!"); 56 | if (nonceBytes.Length == 8) 57 | nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray(); 58 | 59 | var decStream = new MemoryStream(); 60 | using (BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff))) 61 | { 62 | using (BinaryWriter writer = new BinaryWriter(decStream)) 63 | { 64 | while (true) 65 | { 66 | var buffer = reader.ReadBytes(1024); 67 | byte[] dec = new byte[buffer.Length]; 68 | if (buffer.Length > 0) 69 | { 70 | ChaCha20 forDecrypting = new ChaCha20(keyBytes, nonceBytes, 0); 71 | forDecrypting.DecryptBytes(dec, buffer); 72 | writer.Write(dec, 0, dec.Length); 73 | } 74 | else 75 | { 76 | break; 77 | } 78 | } 79 | } 80 | } 81 | return decStream.ToArray(); 82 | } 83 | 84 | public static byte[] HexStringToBytes(string hexStr) 85 | { 86 | if (string.IsNullOrEmpty(hexStr)) 87 | { 88 | return new byte[0]; 89 | } 90 | 91 | if (hexStr.StartsWith("0x") || hexStr.StartsWith("0X")) 92 | { 93 | hexStr = hexStr.Remove(0, 2); 94 | } 95 | 96 | int count = hexStr.Length; 97 | 98 | if (count % 2 == 1) 99 | { 100 | throw new ArgumentException("Invalid length of bytes:" + count); 101 | } 102 | 103 | int byteCount = count / 2; 104 | byte[] result = new byte[byteCount]; 105 | for (int ii = 0; ii < byteCount; ++ii) 106 | { 107 | var tempBytes = Byte.Parse(hexStr.Substring(2 * ii, 2), System.Globalization.NumberStyles.HexNumber); 108 | result[ii] = tempBytes; 109 | } 110 | 111 | return result; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/Downloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace N_m3u8DL_CLI 12 | { 13 | class Downloader 14 | { 15 | private int timeOut = 0; 16 | private int retry = 5; 17 | private int count = 0; 18 | private int segIndex = 0; 19 | private double segDur = 0; 20 | private string fileUrl = string.Empty; 21 | private string savePath = string.Empty; 22 | private string headers = string.Empty; 23 | private string method = string.Empty; 24 | private string key = string.Empty; 25 | private string iv = string.Empty; 26 | private string liveFile = string.Empty; 27 | private long expectByte = -1; 28 | private long startByte = 0; 29 | private bool isLive = false; 30 | private bool isDone = false; 31 | private bool firstSeg = true; 32 | private FileStream liveStream = null; 33 | 34 | public string FileUrl { get => fileUrl; set => fileUrl = value; } 35 | public string SavePath { get => savePath; set => savePath = value; } 36 | public string Headers { get => headers; set => headers = value; } 37 | public string Method { get => method; set => method = value; } 38 | public string Key { get => key; set => key = value; } 39 | public string Iv { get => iv; set => iv = value; } 40 | public bool IsLive { get => isLive; set => isLive = value; } 41 | public int Retry { get => retry; set => retry = value; } 42 | public bool IsDone { get => isDone; set => isDone = value; } 43 | public int SegIndex { get => segIndex; set => segIndex = value; } 44 | public int TimeOut { get => timeOut; set => timeOut = value; } 45 | public FileStream LiveStream { get => liveStream; set => liveStream = value; } 46 | public string LiveFile { get => liveFile; set => liveFile = value; } 47 | public long ExpectByte { get => expectByte; set => expectByte = value; } 48 | public long StartByte { get => startByte; set => startByte = value; } 49 | public double SegDur { get => segDur; set => segDur = value; } 50 | 51 | public static bool EnableChaCha20 { get; set; } = false; 52 | public static string ChaCha20KeyBase64 { get; set; } 53 | public static string ChaCha20NonceBase64 { get; set; } 54 | 55 | //重写WebClinet 56 | //private class WebClient : System.Net.WebClient 57 | //{ 58 | // protected override WebRequest GetWebRequest(Uri uri) 59 | // { 60 | // WebRequest lWebRequest = base.GetWebRequest(uri); 61 | // lWebRequest.Timeout = TimeOut; 62 | // ((HttpWebRequest)lWebRequest).ReadWriteTimeout = TimeOut; 63 | // return lWebRequest; 64 | // } 65 | //} 66 | 67 | //WebClient client = new WebClient(); 68 | 69 | 70 | public void Down() 71 | { 72 | try 73 | { 74 | //直播下载 75 | if (IsLive) 76 | { 77 | IsDone = false; //设置为未完成下载 78 | 79 | if (Method == "NONE" || method.Contains("NOTSUPPORTED")) 80 | { 81 | LOGGER.PrintLine("<" + SegIndex + " Downloading>"); 82 | LOGGER.WriteLine("<" + SegIndex + " Downloading>"); 83 | byte[] segBuff = Global.HttpDownloadFileToBytes(fileUrl, Headers, TimeOut); 84 | //byte[] segBuff = Global.WebClientDownloadToBytes(fileUrl, Headers); 85 | Global.AppendBytesToFileStreamAndDoNotClose(LiveStream, segBuff); 86 | LOGGER.PrintLine("<" + SegIndex + " Complete>\r\n"); 87 | LOGGER.WriteLine("<" + SegIndex + " Complete>"); 88 | IsDone = true; 89 | } 90 | else if (Method == "AES-128") 91 | { 92 | LOGGER.PrintLine("<" + SegIndex + " Downloading>"); 93 | LOGGER.WriteLine("<" + SegIndex + " Downloading>"); 94 | byte[] encryptedBuff = Global.HttpDownloadFileToBytes(fileUrl, Headers, TimeOut); 95 | //byte[] encryptedBuff = Global.WebClientDownloadToBytes(fileUrl, Headers); 96 | byte[] decryptBuff = null; 97 | decryptBuff = Decrypter.AES128Decrypt( 98 | encryptedBuff, 99 | Convert.FromBase64String(Key), 100 | Decrypter.HexStringToBytes(Iv) 101 | ); 102 | Global.AppendBytesToFileStreamAndDoNotClose(LiveStream, decryptBuff); 103 | LOGGER.PrintLine("<" + SegIndex + " Complete>\r\n"); 104 | LOGGER.WriteLine("<" + SegIndex + " Complete>"); 105 | IsDone = true; 106 | } 107 | else 108 | { 109 | //LOGGER.PrintLine("不支持这种加密方式!", LOGGER.Error); 110 | IsDone = true; 111 | } 112 | if (firstSeg && Global.FileSize(LiveFile) != 0) 113 | { 114 | //LOGGER.STOPLOG = false; //记录日志 115 | foreach (string ss in (string[])Global.GetVideoInfo(LiveFile).ToArray(typeof(string))) 116 | { 117 | LOGGER.WriteLine(ss.Trim()); 118 | } 119 | firstSeg = false; 120 | //LOGGER.STOPLOG = true; //停止记录日志 121 | } 122 | HLSLiveDownloader.REC_DUR += SegDur; 123 | if (HLSLiveDownloader.REC_DUR_LIMIT != -1 && HLSLiveDownloader.REC_DUR >= HLSLiveDownloader.REC_DUR_LIMIT) 124 | { 125 | LOGGER.PrintLine(strings.recordLimitReached, LOGGER.Warning); 126 | LOGGER.WriteLine(strings.recordLimitReached); 127 | Environment.Exit(0); //正常退出 128 | } 129 | return; 130 | } 131 | //点播下载 132 | else 133 | { 134 | if (!Directory.Exists(Path.GetDirectoryName(SavePath))) 135 | Directory.CreateDirectory(Path.GetDirectoryName(SavePath)); //新建文件夹 136 | //是否存在文件,存在则不下载 137 | if (File.Exists(Path.GetDirectoryName(savePath) + "\\" + Path.GetFileNameWithoutExtension(savePath) + ".ts")) 138 | { 139 | Global.BYTEDOWN++; //防止被速度监控程序杀死 140 | //Console.WriteLine("Exists " + Path.GetFileNameWithoutExtension(savePath) + ".ts"); 141 | return; 142 | } 143 | //Console.WriteLine("开始下载 " + fileUrl); 144 | //本地文件 145 | if (fileUrl.StartsWith("file:")) 146 | { 147 | Uri t = new Uri(fileUrl); 148 | fileUrl = t.LocalPath; 149 | if (File.Exists(fileUrl)) 150 | { 151 | if (ExpectByte == -1) //没有RANGE 152 | { 153 | FileInfo fi = new FileInfo(fileUrl); 154 | fi.CopyTo(savePath); 155 | Global.BYTEDOWN += fi.Length; 156 | } 157 | else 158 | { 159 | FileStream stream = new FileInfo(fileUrl).OpenRead(); 160 | //seek文件 161 | stream.Seek(StartByte, SeekOrigin.Begin); 162 | Byte[] buffer = new Byte[ExpectByte]; 163 | //从流中读取字节块并将该数据写入给定缓冲区buffer中 164 | stream.Read(buffer, 0, Convert.ToInt32(buffer.Length)); 165 | stream.Close(); 166 | //写出文件 167 | MemoryStream m = new MemoryStream(buffer); 168 | FileStream fs = new FileStream(savePath, FileMode.OpenOrCreate); 169 | m.WriteTo(fs); 170 | m.Close(); 171 | fs.Close(); 172 | m = null; 173 | fs = null; 174 | } 175 | } 176 | } 177 | else 178 | { 179 | //下载 180 | Global.HttpDownloadFile(fileUrl, savePath, TimeOut, Headers, StartByte, ExpectByte); 181 | } 182 | } 183 | if (File.Exists(savePath) && Global.ShouldStop == false) 184 | { 185 | FileInfo fi = new FileInfo(savePath); 186 | if (File.Exists(fi.FullName) && EnableChaCha20) 187 | { 188 | byte[] decryptBuff = Decrypter.CHACHA20Decrypt(File.ReadAllBytes(fi.FullName), Convert.FromBase64String(ChaCha20KeyBase64), Convert.FromBase64String(ChaCha20NonceBase64)); 189 | FileStream fs = new FileStream(Path.GetDirectoryName(SavePath) + "\\" + Path.GetFileNameWithoutExtension(SavePath) + ".ts", FileMode.Create); 190 | fs.Write(decryptBuff, 0, decryptBuff.Length); 191 | fs.Close(); 192 | DownloadManager.DownloadedSize += fi.Length; 193 | fi.Delete(); 194 | } 195 | else if (Method == "NONE" || Method.Contains("NOTSUPPORTED")) 196 | { 197 | fi.MoveTo(Path.GetDirectoryName(SavePath) + "\\" + Path.GetFileNameWithoutExtension(SavePath) + ".ts"); 198 | DownloadManager.DownloadedSize += fi.Length; 199 | //Console.WriteLine(Path.GetFileNameWithoutExtension(savePath) + " Completed."); 200 | } 201 | else if (File.Exists(fi.FullName) 202 | && Method == "AES-128") 203 | { 204 | //解密 205 | try 206 | { 207 | byte[] decryptBuff = null; 208 | if(fileUrl.Contains(".51cto.com/")) //使用AES-128-ECB模式解密 209 | { 210 | decryptBuff = Decrypter.AES128Decrypt( 211 | fi.FullName, 212 | Convert.FromBase64String(Key), 213 | Decrypter.HexStringToBytes(Iv), 214 | System.Security.Cryptography.CipherMode.ECB 215 | ); 216 | } 217 | else 218 | { 219 | decryptBuff = Decrypter.AES128Decrypt( 220 | fi.FullName, 221 | Convert.FromBase64String(Key), 222 | Decrypter.HexStringToBytes(Iv) 223 | ); 224 | } 225 | FileStream fs = new FileStream(Path.GetDirectoryName(savePath) + "\\" + Path.GetFileNameWithoutExtension(savePath) + ".ts", FileMode.Create); 226 | fs.Write(decryptBuff, 0, decryptBuff.Length); 227 | fs.Close(); 228 | DownloadManager.DownloadedSize += fi.Length; 229 | fi.Delete(); 230 | //Console.WriteLine(Path.GetFileNameWithoutExtension(savePath) + " Completed & Decrypted."); 231 | } 232 | catch (Exception ex) 233 | { 234 | LOGGER.PrintLine(ex.Message, LOGGER.Error); 235 | LOGGER.WriteLineError(ex.Message); 236 | Thread.Sleep(3000); 237 | Environment.Exit(-1); 238 | } 239 | } 240 | else 241 | { 242 | LOGGER.WriteLineError(strings.SomethingWasWrong); 243 | LOGGER.PrintLine(strings.SomethingWasWrong, LOGGER.Error); 244 | return; 245 | } 246 | return; 247 | } 248 | } 249 | catch (Exception ex) 250 | { 251 | LOGGER.WriteLineError(ex.Message); 252 | if (ex.Message.Contains("404") || ex.Message.Contains("400"))//(400) 错误的请求,片段过期会提示400错误 253 | { 254 | IsDone = true; 255 | return; 256 | } 257 | else if (IsLive && count++ < Retry) 258 | { 259 | Thread.Sleep(2000);//直播一般3-6秒一个片段 260 | Down(); 261 | } 262 | } 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /N_m3u8DL-CLI/FFmpeg.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace N_m3u8DL_CLI 10 | { 11 | class FFmpeg 12 | { 13 | public static string FFMPEG_PATH = "ffmpeg"; 14 | public static string REC_TIME = ""; //录制日期 15 | 16 | public static string OutPutPath { get; set; } = string.Empty; 17 | public static string ReportFile { get; set; } = string.Empty; 18 | public static bool UseAACFilter { get; set; } = false; //是否启用滤镜 19 | public static bool WriteDate { get; set; } = true; //是否写入录制日期 20 | 21 | public static void Merge(string[] files, string muxFormat, bool fastStart, 22 | string poster = "", string audioName = "", string title = "", 23 | string copyright = "", string comment = "", string encodingTool = "") 24 | { 25 | string dateString = string.IsNullOrEmpty(REC_TIME) ? DateTime.Now.ToString("o") : REC_TIME; 26 | 27 | //同名文件已存在的共存策略 28 | if (File.Exists($"{OutPutPath}.{muxFormat.ToLower()}")) 29 | { 30 | OutPutPath = Path.Combine(Path.GetDirectoryName(OutPutPath), 31 | Path.GetFileName(OutPutPath) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")); 32 | } 33 | 34 | string command = "-loglevel warning -i concat:\""; 35 | string data = string.Empty; 36 | string ddpAudio = string.Empty; 37 | string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic"; 38 | ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(OutPutPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(OutPutPath + ".mp4")}.txt") : "") ; 39 | if (!string.IsNullOrEmpty(ddpAudio)) UseAACFilter = false; 40 | 41 | 42 | foreach (string t in files) 43 | { 44 | command += Path.GetFileName(t) + "|"; 45 | } 46 | 47 | switch (muxFormat.ToUpper()) 48 | { 49 | case ("MP4"): 50 | command += "\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\""); 51 | command += " " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\""); 52 | command += 53 | $" -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) 54 | + (WriteDate ? " -metadata date=\"" + dateString + "\"" : "") + 55 | " -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title + 56 | "\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment + 57 | $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler_name=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" "; 58 | command += (string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 handler_name=\"DD+\" -metadata:s:a:0 handler=\"DD+\" "); 59 | if (fastStart) 60 | command += "-movflags +faststart"; 61 | command += " -c copy -y " + (UseAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + OutPutPath + ".mp4\""; 62 | break; 63 | case ("MKV"): 64 | command += "\" -map 0 -c copy -y " + (UseAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + OutPutPath + ".mkv\""; 65 | break; 66 | case ("FLV"): 67 | command += "\" -map 0 -c copy -y " + (UseAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + OutPutPath + ".flv\""; 68 | break; 69 | case ("TS"): 70 | command += "\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + OutPutPath + ".ts\""; 71 | break; 72 | case ("VTT"): 73 | command += "\" -map 0 -y \"" + OutPutPath + ".srt\""; //Convert To Srt 74 | break; 75 | case ("EAC3"): 76 | command += "\" -map 0:a -c copy -y \"" + OutPutPath + ".eac3\""; 77 | break; 78 | case ("AAC"): 79 | command += "\" -map 0:a -c copy -y \"" + OutPutPath + ".m4a\""; 80 | break; 81 | case ("AC3"): 82 | command += "\" -map 0:a -c copy -y \"" + OutPutPath + ".ac3\""; 83 | break; 84 | 85 | } 86 | 87 | Run(FFMPEG_PATH, command, Path.GetDirectoryName(files[0])); 88 | LOGGER.WriteLine(strings.ffmpegDone); 89 | //Console.WriteLine(command); 90 | } 91 | 92 | public static void ConvertToMPEGTS(string file) 93 | { 94 | if (Global.VIDEO_TYPE == "H264") 95 | { 96 | Run(FFMPEG_PATH, 97 | "-loglevel quiet -i \"" + file + "\" -map 0 -c copy -copy_unknown -f mpegts -bsf:v h264_mp4toannexb \"" 98 | + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts\"", 99 | Path.GetDirectoryName(file)); 100 | if (File.Exists(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts")) 101 | { 102 | File.Delete(file); 103 | File.Move(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts", file); 104 | } 105 | } 106 | else if (Global.VIDEO_TYPE == "H265") 107 | { 108 | Run(FFMPEG_PATH, 109 | "-loglevel quiet -i \"" + file + "\" -map 0 -c copy -copy_unknown -f mpegts -bsf:v hevc_mp4toannexb \"" 110 | + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts\"", 111 | Path.GetDirectoryName(file)); 112 | if (File.Exists(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts")) 113 | { 114 | File.Delete(file); 115 | File.Move(Path.GetDirectoryName(file) + "\\" + Path.GetFileNameWithoutExtension(file) + "[MPEGTS].ts", file); 116 | } 117 | } 118 | else 119 | { 120 | LOGGER.WriteLineError("Unkown Video Type"); 121 | } 122 | } 123 | 124 | public static void Run(string path, string args, string workDir) 125 | { 126 | string nowDir = Directory.GetCurrentDirectory(); //当前工作路径 127 | Directory.SetCurrentDirectory(workDir); 128 | Process p = new Process();//建立外部调用线程 129 | p.StartInfo.FileName = path;//要调用外部程序的绝对路径 130 | Environment.SetEnvironmentVariable("FFREPORT", "file=" + ReportFile + ":level=32"); //兼容XP系统 131 | //p.StartInfo.Environment.Add("FFREPORT", "file=" + ReportFile + ":level=32"); 132 | p.StartInfo.Arguments = args;//参数(这里就是FFMPEG的参数了) 133 | p.StartInfo.UseShellExecute = false;//不使用操作系统外壳程序启动线程(一定为FALSE,详细的请看MSDN) 134 | p.StartInfo.RedirectStandardError = true;//把外部程序错误输出写到StandardError流中(这个一定要注意,FFMPEG的所有输出信息,都为错误输出流,用StandardOutput是捕获不到任何消息的...这是我耗费了2个多月得出来的经验...mencoder就是用standardOutput来捕获的) 135 | p.StartInfo.CreateNoWindow = false;//不创建进程窗口 136 | p.ErrorDataReceived += new DataReceivedEventHandler(Output);//外部程序(这里是FFMPEG)输出流时候产生的事件,这里是把流的处理过程转移到下面的方法中,详细请查阅MSDN 137 | p.StartInfo.StandardErrorEncoding = Encoding.UTF8; 138 | p.Start();//启动线程 139 | p.BeginErrorReadLine();//开始异步读取 140 | p.WaitForExit();//阻塞等待进程结束 141 | p.Close();//关闭进程 142 | p.Dispose();//释放资源 143 | Environment.SetEnvironmentVariable("FFREPORT", null); //兼容XP系统 144 | Directory.SetCurrentDirectory(nowDir); 145 | } 146 | 147 | private static void Output(object sendProcess, DataReceivedEventArgs output) 148 | { 149 | if (!String.IsNullOrEmpty(output.Data)) 150 | { 151 | LOGGER.PrintLine(output.Data, LOGGER.Warning); 152 | } 153 | } 154 | 155 | public static bool CheckMPEGTS(string file) 156 | { 157 | //放行杜比视界或纯音频文件 158 | if (Global.VIDEO_TYPE == "DV" || Global.AUDIO_TYPE != "") 159 | return true; 160 | //如果是多分片,也认为不是MPEGTS 161 | if (DownloadManager.PartsCount > 1) 162 | return false; 163 | 164 | using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read)) 165 | { 166 | byte[] firstByte = new byte[1]; 167 | fs.Read(firstByte, 0, 1); 168 | //第一字节的16进制字符串 169 | string _1_byte_str = Convert.ToString(firstByte[0], 16); 170 | //syncword不为47就不处理 171 | if (_1_byte_str != "47") 172 | return false; 173 | } 174 | return true; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/HLSLiveDownloader.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Timers; 10 | 11 | namespace N_m3u8DL_CLI 12 | { 13 | class HLSLiveDownloader 14 | { 15 | public static int REC_DUR_LIMIT = -1; //默认不限制录制时长 16 | public static double REC_DUR = 0; //已录制时长 17 | private string liveFile = string.Empty; 18 | private string jsonFile = string.Empty; 19 | private string headers = string.Empty; 20 | private string downDir = string.Empty; 21 | private FileStream liveStream = null; 22 | private double targetduration = 10; 23 | private bool isFirstJson = true; 24 | 25 | public double TotalDuration { get; set; } 26 | public string Headers { get => headers; set => headers = value; } 27 | public string DownDir { get => downDir; set => downDir = value; } 28 | public FileStream LiveStream { get => liveStream; set => liveStream = value; } 29 | public string LiveFile { get => liveFile; set => liveFile = value; } 30 | 31 | ArrayList toDownList = new ArrayList(); //所有待下载的列表 32 | System.Timers.Timer timer = new System.Timers.Timer(); 33 | Downloader sd = new Downloader(); //只有一个实例 34 | 35 | public void TimerStart() 36 | { 37 | timer.Enabled = true; 38 | //timer.Interval = (targetduration - 2) * 1000; //执行间隔时间,单位为毫秒 39 | timer.Start(); 40 | timer.Elapsed += new ElapsedEventHandler(UpdateList); 41 | UpdateList(timer, new EventArgs()); //立即执行一次 42 | Record(); 43 | } 44 | 45 | public void TimerStop() 46 | { 47 | timer.Stop(); 48 | } 49 | 50 | //更新列表 51 | private void UpdateList(object source, EventArgs e) 52 | { 53 | jsonFile = Path.Combine(DownDir, "meta.json"); 54 | if (!File.Exists(jsonFile)) 55 | { 56 | TimerStop(); 57 | return; 58 | } 59 | string jsonContent = File.ReadAllText(jsonFile); 60 | JObject initJson = JObject.Parse(jsonContent); 61 | string m3u8Url = initJson["m3u8"].Value(); 62 | targetduration = initJson["m3u8Info"]["targetDuration"].Value(); 63 | TotalDuration = initJson["m3u8Info"]["totalDuration"].Value(); 64 | timer.Interval = Math.Abs(TotalDuration - targetduration) * 1000;//设置定时器运行间隔 65 | if (timer.Interval <= 1000) timer.Interval = 10000; 66 | JArray lastSegments = JArray.Parse(initJson["m3u8Info"]["segments"][0].ToString().Trim()); //上次的分段,用于比对新分段 67 | ArrayList tempList = new ArrayList(); //所有待下载的列表 68 | tempList.Clear(); 69 | foreach (JObject seg in lastSegments) 70 | { 71 | tempList.Add(seg.ToString()); 72 | } 73 | 74 | if(isFirstJson) 75 | { 76 | toDownList = tempList; 77 | isFirstJson = false; 78 | return; 79 | } 80 | 81 | Parser parser = new Parser(); 82 | parser.Headers = Headers; 83 | parser.DownDir = Path.GetDirectoryName(jsonFile); 84 | parser.M3u8Url = m3u8Url; 85 | parser.LiveStream = true; 86 | parser.Parse(); //产生新的json文件 87 | 88 | jsonContent = File.ReadAllText(jsonFile); 89 | initJson = JObject.Parse(jsonContent); 90 | JArray segments = JArray.Parse(initJson["m3u8Info"]["segments"][0].ToString()); //大分组 91 | foreach (JObject seg in segments) 92 | { 93 | if (!tempList.Contains(seg.ToString())) 94 | { 95 | toDownList.Add(seg.ToString()); //加入真正的待下载队列 96 | //Console.WriteLine(seg.ToString()); 97 | } 98 | } 99 | if (toDownList.Count > 0) 100 | Record(); 101 | } 102 | 103 | //public void TryDownload() 104 | //{ 105 | // Thread t = new Thread(Download); 106 | // while (toDownList.Count != 0) 107 | // { 108 | // t = new Thread(Download); 109 | // t.Start(); 110 | // t.Join(); 111 | // while (sd.IsDone != true) ; //忙等待 112 | // if (toDownList.Count > 0) 113 | // toDownList.RemoveAt(0); //下完删除一项 114 | // } 115 | // Console.WriteLine("Waiting..."); 116 | //} 117 | 118 | private void Record() 119 | { 120 | while (toDownList.Count > 0 && (sd.FileUrl != "" ? sd.IsDone : true)) 121 | { 122 | JObject info = JObject.Parse(toDownList[0].ToString()); 123 | int index = info["index"].Value(); 124 | sd.FileUrl = info["segUri"].Value(); 125 | sd.Method = info["method"].Value(); 126 | if (sd.Method != "NONE") 127 | { 128 | sd.Key = info["key"].Value(); 129 | sd.Iv = info["iv"].Value(); 130 | } 131 | sd.TimeOut = (int)timer.Interval - 1000;//超时时间不超过下次执行时间 132 | if (sd.TimeOut <= 0) sd.TimeOut = (int)timer.Interval; 133 | sd.SegIndex = index; 134 | sd.Headers = Headers; 135 | sd.SegDur = info["duration"].Value(); 136 | sd.IsLive = true; //标记为直播 137 | sd.LiveFile = LiveFile; 138 | sd.LiveStream = LiveStream; 139 | sd.Down(); //开始下载 140 | while (sd.IsDone != true) { Thread.Sleep(1); }; //忙等待 Thread.Sleep(1) 可防止cpu 100% 防止电脑风扇狂转 141 | if (toDownList.Count > 0) 142 | toDownList.RemoveAt(0); //下完删除一项 143 | } 144 | LOGGER.PrintLine("Waiting...", LOGGER.Warning); 145 | LOGGER.WriteLine("Waiting..."); 146 | } 147 | 148 | //检测是否有新分片 149 | private bool isNewSeg() 150 | { 151 | if (toDownList.Count > 0) 152 | return true; 153 | return false; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/HLSTags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N_m3u8DL_CLI 8 | { 9 | class HLSTags 10 | { 11 | public static string ext_m3u = "#EXTM3U"; 12 | public static string ext_x_targetduration = "#EXT-X-TARGETDURATION"; 13 | public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; 14 | public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; 15 | public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; 16 | public static string ext_x_media = "#EXT-X-MEDIA"; 17 | public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; 18 | public static string ext_x_key = "#EXT-X-KEY"; 19 | public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; 20 | public static string ext_x_version = "#EXT-X-VERSION"; 21 | public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; 22 | public static string ext_x_endlist = "#EXT-X-ENDLIST"; 23 | public static string extinf = "#EXTINF"; 24 | public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; 25 | public static string ext_x_byterange = "#EXT-X-BYTERANGE"; 26 | public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; 27 | public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; 28 | public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; 29 | public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; 30 | public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; 31 | public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; 32 | public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; 33 | public static string ext_x_cue_end = "#EXT-X-CUE-IN"; 34 | public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; 35 | public static string ext_x_map = "#EXT-X-MAP"; 36 | public static string ext_x_start = "#EXT-X-START"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/IqJsonParser.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace N_m3u8DL_CLI 10 | { 11 | class IqJsonParser 12 | { 13 | public static string Parse(string downDir, string json) 14 | { 15 | JObject jObject = JObject.Parse(json); 16 | var aClips = jObject["payload"]["wm_a"]["audio_track1"]["files"].Value(); 17 | var vClips = jObject["payload"]["wm_a"]["video_track1"]["files"].Value(); 18 | 19 | var codecsList = new List(); 20 | 21 | var audioPath = ""; 22 | var videoPath = ""; 23 | var audioInitPath = ""; 24 | var videoInitPath = ""; 25 | 26 | if (aClips.Count > 0) 27 | { 28 | var init = jObject["payload"]["wm_a"]["audio_track1"]["codec_init"].Value(); 29 | byte[] bytes = Convert.FromBase64String(init); 30 | //输出init文件 31 | audioInitPath = Path.Combine(downDir, "iqAudioInit.mp4"); 32 | File.WriteAllBytes(audioInitPath, bytes); 33 | StringBuilder sb = new StringBuilder(); 34 | sb.AppendLine("#EXTM3U"); 35 | sb.AppendLine("#EXT-X-VERSION:3"); 36 | sb.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); 37 | sb.AppendLine("#CREATED-BY:N_m3u8DL-CLI"); 38 | sb.AppendLine($"#EXT-CODEC:{jObject["payload"]["wm_a"]["audio_track1"]["codec"].Value()}"); 39 | sb.AppendLine($"#EXT-KID:{jObject["payload"]["wm_a"]["audio_track1"]["key_id"].Value()}"); 40 | sb.AppendLine($"#EXT-X-MAP:URI=\"{new Uri(Path.Combine(downDir + "(Audio)", "iqAudioInit.mp4")).ToString()}\""); 41 | sb.AppendLine("#EXT-X-KEY:METHOD=PLZ-KEEP-RAW,URI=\"None\""); 42 | foreach (var a in aClips) 43 | { 44 | sb.AppendLine($"#EXTINF:{a["duration_second"].ToString()}"); 45 | sb.AppendLine(a["file_name"].Value()); 46 | } 47 | sb.AppendLine("#EXT-X-ENDLIST"); 48 | //输出m3u8文件 49 | var _path = Path.Combine(downDir, "iqAudio.m3u8"); 50 | File.WriteAllText(_path, sb.ToString()); 51 | audioPath = new Uri(_path).ToString(); 52 | codecsList.Add(jObject["payload"]["wm_a"]["audio_track1"]["codec"].Value()); 53 | } 54 | 55 | if (vClips.Count > 0) 56 | { 57 | var init = jObject["payload"]["wm_a"]["video_track1"]["codec_init"].Value(); 58 | byte[] bytes = Convert.FromBase64String(init); 59 | //输出init文件 60 | videoInitPath = Path.Combine(downDir, "iqVideoInit.mp4"); 61 | File.WriteAllBytes(videoInitPath, bytes); 62 | StringBuilder sb = new StringBuilder(); 63 | sb.AppendLine("#EXTM3U"); 64 | sb.AppendLine("#EXT-X-VERSION:3"); 65 | sb.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); 66 | sb.AppendLine("#CREATED-BY:N_m3u8DL-CLI"); 67 | sb.AppendLine($"#EXT-CODEC:{jObject["payload"]["wm_a"]["video_track1"]["codec"].Value()}"); 68 | sb.AppendLine($"#EXT-KID:{jObject["payload"]["wm_a"]["video_track1"]["key_id"].Value()}"); 69 | sb.AppendLine($"#EXT-X-MAP:URI=\"{new Uri(videoInitPath).ToString()}\""); 70 | sb.AppendLine("#EXT-X-KEY:METHOD=PLZ-KEEP-RAW,URI=\"None\""); 71 | foreach (var a in vClips) 72 | { 73 | var start = a["seekable"]["pos_start"].Value(); 74 | var size = a["size"].Value(); 75 | sb.AppendLine($"#EXTINF:{a["duration_second"].ToString()}"); 76 | sb.AppendLine($"#EXT-X-BYTERANGE:{size}@{start}"); 77 | sb.AppendLine(a["file_name"].Value()); 78 | } 79 | sb.AppendLine("#EXT-X-ENDLIST"); 80 | //输出m3u8文件 81 | var _path = Path.Combine(downDir, "iqVideo.m3u8"); 82 | File.WriteAllText(_path, sb.ToString()); 83 | videoPath = new Uri(_path).ToString(); 84 | codecsList.Add(jObject["payload"]["wm_a"]["video_track1"]["codec"].Value()); 85 | } 86 | 87 | var content = ""; 88 | if ((videoPath == "" && audioPath != "") || Global.VIDEO_TYPE == "IGNORE") 89 | { 90 | return audioPath; 91 | } 92 | else if (audioPath == "" && videoPath != "") 93 | { 94 | return videoPath; 95 | } 96 | else 97 | { 98 | if (!Directory.Exists(downDir + "(Audio)")) 99 | Directory.CreateDirectory(downDir + "(Audio)"); 100 | var _path = Path.Combine(downDir + "(Audio)", "iqAudio.m3u8"); 101 | var _pathInit = Path.Combine(downDir + "(Audio)", "iqAudioInit.mp4"); 102 | File.Copy(new Uri(audioPath).LocalPath, _path, true); 103 | File.Copy(new Uri(audioInitPath).LocalPath, _pathInit, true); 104 | audioPath = new Uri(_path).ToString(); 105 | content = $"#EXTM3U\r\n" + 106 | $"#EXT-X-MEDIA:TYPE=AUDIO,URI=\"{audioPath}\",GROUP-ID=\"default-audio-group\",NAME=\"stream_0\",AUTOSELECT=YES,CHANNELS=\"0\"\r\n" + 107 | $"#EXT-X-STREAM-INF:BANDWIDTH=99999,CODECS=\"{string.Join(",", codecsList)}\",RESOLUTION=0x0,AUDIO=\"default-audio-group\"\r\n" + 108 | $"{videoPath}"; 109 | } 110 | 111 | var _masterPath = Path.Combine(downDir, "master.m3u8"); 112 | File.WriteAllText(_masterPath, content); 113 | return new Uri(_masterPath).ToString(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/LOGGER.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace N_m3u8DL_CLI 11 | { 12 | class LOGGER 13 | { 14 | public const int Default = 1; 15 | public const int Error = 2; 16 | public const int Warning = 3; 17 | 18 | public static string LOGFILE; 19 | public static bool STOPLOG = false; 20 | public static string FindLog(string dir) 21 | { 22 | DirectoryInfo d = new DirectoryInfo(dir); 23 | foreach (FileInfo fi in d.GetFiles()) 24 | { 25 | if (fi.Extension.ToUpper() == ".LOG") 26 | { 27 | return fi.FullName; 28 | } 29 | } 30 | return ""; 31 | } 32 | 33 | public static void InitLog() 34 | { 35 | if (!Directory.Exists(Path.GetDirectoryName(LOGFILE)))//若文件夹不存在则新建文件夹 36 | Directory.CreateDirectory(Path.GetDirectoryName(LOGFILE)); //新建文件夹 37 | //若文件存在则加序号 38 | int index = 1; 39 | var fileName = Path.GetFileNameWithoutExtension(LOGFILE); 40 | while (File.Exists(LOGFILE)) 41 | { 42 | LOGFILE = Path.Combine(Path.GetDirectoryName(LOGFILE), $"{fileName}-{index++}.log"); 43 | } 44 | string file = LOGFILE; 45 | string now = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); 46 | string init = "LOG " + DateTime.Now.ToString("yyyy/MM/dd") + "\r\n" 47 | + "Save Path: " + Path.GetDirectoryName(LOGFILE) + "\r\n" 48 | + "Task Start: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + "\r\n" 49 | + "Task CommandLine: " + Environment.CommandLine; 50 | 51 | if (File.Exists(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), "N_m3u8DL-CLI.args.txt"))) 52 | { 53 | init += "\r\nAdditional Args: " + File.ReadAllText(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), "N_m3u8DL-CLI.args.txt")); //解析命令行 54 | } 55 | 56 | init += "\r\n\r\n"; 57 | File.WriteAllText(file, init, Encoding.UTF8); 58 | } 59 | 60 | //读写锁机制,当资源被占用,其他线程等待 61 | static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); 62 | 63 | public static void PrintLine(string text, int printLevel = 1) 64 | { 65 | int windowWith = 63; 66 | try 67 | { 68 | windowWith = Console.WindowWidth; 69 | } 70 | catch (Exception e) 71 | { 72 | // empty 73 | } 74 | switch (printLevel) 75 | { 76 | case 0: 77 | Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); 78 | Console.WriteLine(" ".PadRight(12) + " " + text); 79 | break; 80 | case 1: 81 | Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); 82 | Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " "); 83 | Console.WriteLine(text); 84 | break; 85 | case 2: 86 | Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); 87 | Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " "); 88 | Console.ForegroundColor = ConsoleColor.Red; 89 | Console.WriteLine(text); 90 | Console.ResetColor(); 91 | break; 92 | case 3: 93 | Console.Write("\r" + new string(' ', windowWith - 1) + "\r"); 94 | Console.Write(DateTime.Now.ToString("HH:mm:ss.fff") + " "); 95 | Console.ForegroundColor = ConsoleColor.DarkYellow; 96 | Console.WriteLine(text); 97 | Console.ResetColor(); 98 | break; 99 | } 100 | } 101 | 102 | public static void WriteLine(string text) 103 | { 104 | if (STOPLOG) 105 | return; 106 | if (!File.Exists(LOGFILE)) 107 | return; 108 | 109 | try 110 | { 111 | string file = LOGFILE; 112 | //进入写入 113 | LogWriteLock.EnterWriteLock(); 114 | using (StreamWriter sw = File.AppendText(file)) 115 | { 116 | sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " / (NORMAL) " + text, Encoding.UTF8); 117 | } 118 | } 119 | catch (Exception) 120 | { 121 | 122 | } 123 | finally 124 | { 125 | //释放占用 126 | LogWriteLock.ExitWriteLock(); 127 | } 128 | } 129 | 130 | public static void WriteLineError(string text) 131 | { 132 | if (!File.Exists(LOGFILE)) 133 | return; 134 | try 135 | { 136 | string file = LOGFILE; 137 | //进入写入 138 | LogWriteLock.EnterWriteLock(); 139 | using (StreamWriter sw = File.AppendText(file)) 140 | { 141 | sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " / (ERROR) " + text, Encoding.UTF8); 142 | } 143 | } 144 | catch (Exception) 145 | { 146 | 147 | } 148 | finally 149 | { 150 | //释放占用 151 | LogWriteLock.ExitWriteLock(); 152 | } 153 | } 154 | 155 | public static void Show(string text) 156 | { 157 | Console.ForegroundColor = ConsoleColor.Red; 158 | Console.WriteLine(DateTime.Now.ToString("o") + " " + text); 159 | while (Console.ForegroundColor == ConsoleColor.Red) 160 | Console.ResetColor(); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/MyOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace N_m3u8DL_CLI 10 | { 11 | internal class MyOptions 12 | { 13 | [Value(0, Hidden = true, MetaName = "Input Source", HelpText = "Help_input", ResourceType = typeof(strings))] 14 | public string Input { get; set; } 15 | 16 | [Option("workDir", HelpText = "Help_workDir", ResourceType = typeof(strings))] 17 | public string WorkDir { get; set; } 18 | 19 | [Option("saveName", HelpText = "Help_saveName", ResourceType = typeof(strings))] 20 | public string SaveName { get; set; } = ""; 21 | 22 | [Option("baseUrl", HelpText = "Help_baseUrl", ResourceType = typeof(strings))] 23 | public string BaseUrl { get; set; } 24 | 25 | [Option("headers", HelpText = "Help_headers", ResourceType = typeof(strings))] 26 | public string Headers { get; set; } = ""; 27 | 28 | [Option("maxThreads", Default = 32U, HelpText = "Help_maxThreads", ResourceType = typeof(strings))] 29 | public uint MaxThreads { get; set; } 30 | 31 | [Option("minThreads", Default = 16U, HelpText = "Help_minThreads", ResourceType = typeof(strings))] 32 | public uint MinThreads { get; set; } 33 | 34 | [Option("retryCount", Default = 15U, HelpText = "Help_retryCount", ResourceType = typeof(strings))] 35 | public uint RetryCount { get; set; } 36 | 37 | [Option("timeOut", Default = 10U, HelpText = "Help_timeOut", ResourceType = typeof(strings))] 38 | public uint TimeOut { get; set; } 39 | 40 | [Option("muxSetJson", HelpText = "Help_muxSetJson", ResourceType = typeof(strings))] 41 | public string MuxSetJson { get; set; } 42 | 43 | [Option("useKeyFile", HelpText = "Help_useKeyFile", ResourceType = typeof(strings))] 44 | public string UseKeyFile { get; set; } 45 | 46 | [Option("useKeyBase64", HelpText = "Help_useKeyBase64", ResourceType = typeof(strings))] 47 | public string UseKeyBase64 { get; set; } 48 | 49 | [Option("useKeyIV", HelpText = "Help_useKeyIV", ResourceType = typeof(strings))] 50 | public string UseKeyIV { get; set; } 51 | 52 | [Option("downloadRange", HelpText = "Help_downloadRange", ResourceType = typeof(strings))] 53 | public string DownloadRange { get; set; } 54 | 55 | [Option("liveRecDur", HelpText = "Help_liveRecDur", ResourceType = typeof(strings))] 56 | public string LiveRecDur { get; set; } 57 | 58 | [Option("stopSpeed", HelpText = "Help_stopSpeed", ResourceType = typeof(strings))] 59 | public long StopSpeed { get; set; } = 0L; 60 | 61 | [Option("maxSpeed", HelpText = "Help_maxSpeed", ResourceType = typeof(strings))] 62 | public long MaxSpeed { get; set; } = 0L; 63 | 64 | [Option("proxyAddress", HelpText = "Help_proxyAddress", ResourceType = typeof(strings))] 65 | public string ProxyAddress { get; set; } 66 | 67 | [Option("enableDelAfterDone", HelpText = "Help_enableDelAfterDone", ResourceType = typeof(strings))] 68 | public bool EnableDelAfterDone { get; set; } 69 | 70 | [Option("enableMuxFastStart", HelpText = "Help_enableMuxFastStart", ResourceType = typeof(strings))] 71 | public bool EnableMuxFastStart { get; set; } 72 | 73 | [Option("enableBinaryMerge", HelpText = "Help_enableBinaryMerge", ResourceType = typeof(strings))] 74 | public bool EnableBinaryMerge { get; set; } 75 | 76 | [Option("enableParseOnly", HelpText = "Help_enableParseOnly", ResourceType = typeof(strings))] 77 | public bool EnableParseOnly { get; set; } 78 | 79 | [Option("enableAudioOnly", HelpText = "Help_enableAudioOnly", ResourceType = typeof(strings))] 80 | public bool EnableAudioOnly { get; set; } 81 | 82 | [Option("disableDateInfo", HelpText = "Help_disableDateInfo", ResourceType = typeof(strings))] 83 | public bool DisableDateInfo { get; set; } 84 | 85 | [Option("disableIntegrityCheck", HelpText = "Help_disableIntegrityCheck", ResourceType = typeof(strings))] 86 | public bool DisableIntegrityCheck { get; set; } 87 | 88 | [Option("noMerge", HelpText = "Help_noMerge", ResourceType = typeof(strings))] 89 | public bool NoMerge { get; set; } 90 | 91 | [Option("noProxy", HelpText = "Help_noProxy", ResourceType = typeof(strings))] 92 | public bool NoProxy { get; set; } 93 | 94 | [Option("registerUrlProtocol", HelpText = "Help_registerUrlProtocol", ResourceType = typeof(strings))] 95 | public bool RegisterUrlProtocol { get; set; } 96 | 97 | [Option("unregisterUrlProtocol", HelpText = "Help_unregisterUrlProtocol", ResourceType = typeof(strings))] 98 | public bool UnregisterUrlProtocol { get; set; } 99 | 100 | [Option("enableChaCha20", HelpText = "enableChaCha20")] 101 | public bool EnableChaCha20 { get; set; } 102 | 103 | [Option("chaCha20KeyBase64", HelpText = "ChaCha20KeyBase64")] 104 | public string ChaCha20KeyBase64 { get; set; } 105 | 106 | [Option("chaCha20NonceBase64", HelpText = "ChaCha20NonceBase64")] 107 | public string ChaCha20NonceBase64 { get; set; } 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/N_m3u8DL-CLI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Debug 8 | AnyCPU 9 | {4FB61439-B738-46AC-B3AF-2BF72150D057} 10 | Exe 11 | N_m3u8DL_CLI 12 | N_m3u8DL-CLI 13 | v4.6 14 | 512 15 | true 16 | 17 | 18 | 19 | 20 | 21 | x86 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | AnyCPU 33 | pdbonly 34 | true 35 | bin\Release\ 36 | TRACE 37 | prompt 38 | 4 39 | false 40 | 41 | 42 | logo_3Iv_icon.ico 43 | 44 | 45 | 46 | ..\packages\BrotliSharpLib.0.3.3\lib\net451\BrotliSharpLib.dll 47 | 48 | 49 | ..\packages\CommandLineParser.2.8.0\lib\net45\CommandLine.dll 50 | 51 | 52 | ..\packages\Costura.Fody.4.1.0\lib\net40\Costura.dll 53 | 54 | 55 | 56 | 57 | 58 | ..\packages\TaskScheduler.2.8.7\lib\net452\Microsoft.Win32.TaskScheduler.dll 59 | 60 | 61 | ..\packages\HttpToSocks5Proxy.1.4.0\lib\net45\MihaZupan.HttpToSocks5Proxy.dll 62 | 63 | 64 | 65 | ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll 66 | 67 | 68 | ..\packages\NiL.JS.2.5.1428\lib\net45\NiL.JS.dll 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ..\packages\UACHelper.1.3.0.5\lib\net40\UACHelper.dll 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | True 117 | True 118 | strings.resx 119 | 120 | 121 | True 122 | True 123 | strings.en-US.resx 124 | 125 | 126 | True 127 | True 128 | strings.zh-TW.resx 129 | 130 | 131 | 132 | 133 | 134 | Designer 135 | 136 | 137 | 138 | 139 | 140 | {420B2830-E718-11CF-893D-00A0C9054228} 141 | 1 142 | 0 143 | 0 144 | tlbimp 145 | False 146 | True 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | PublicResXFileCodeGenerator 155 | strings.en-US.Designer.cs 156 | Designer 157 | 158 | 159 | PublicResXFileCodeGenerator 160 | strings.Designer.cs 161 | Designer 162 | 163 | 164 | PublicResXFileCodeGenerator 165 | strings.zh-TW.Designer.cs 166 | Designer 167 | 168 | 169 | 170 | 171 | 172 | 这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/ProgressReporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N_m3u8DL_CLI 8 | { 9 | class ProgressReporter 10 | { 11 | private static string speed = ""; 12 | private static string progress = ""; 13 | 14 | static object lockThis = new object(); 15 | public static void Report(string progress, string speed) 16 | { 17 | lock (lockThis) 18 | { 19 | int windowWith = 63; 20 | try 21 | { 22 | windowWith = Console.WindowWidth; 23 | } 24 | catch (Exception e) 25 | { 26 | // empty 27 | } 28 | if (!string.IsNullOrEmpty(progress)) ProgressReporter.progress = progress; 29 | if (!string.IsNullOrEmpty(speed)) ProgressReporter.speed = speed; 30 | string now = DateTime.Now.ToString("HH:mm:ss.000"); 31 | var sub = windowWith - 4 - ProgressReporter.progress.Length - ProgressReporter.speed.Length - now.Length; 32 | if (sub <= 0) sub = 0; 33 | string print = now + " " + ProgressReporter.progress + " " + ProgressReporter.speed + new string(' ', sub); 34 | Console.Write("\r" + print + "\r"); 35 | //Console.Write(print); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // 有关程序集的一般信息由以下 6 | // 控制。更改这些特性值可修改 7 | // 与程序集关联的信息。 8 | [assembly: AssemblyTitle("N_m3u8DL-CLI")] 9 | [assembly: AssemblyDescription("一款命令行m3u8下载器")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("nilaoda")] 12 | [assembly: AssemblyProduct("N_m3u8DL-CLI")] 13 | [assembly: AssemblyCopyright("Copyright © 2022")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // 将 ComVisible 设置为 false 会使此程序集中的类型 18 | //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 19 | //请将此类型的 ComVisible 特性设置为 true。 20 | [assembly: ComVisible(false)] 21 | 22 | // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID 23 | [assembly: Guid("4fb61439-b738-46ac-b3af-2bf72150d057")] 24 | 25 | // 程序集的版本信息由下列四个值组成: 26 | // 27 | // 主版本 28 | // 次版本 29 | // 生成号 30 | // 修订号 31 | // 32 | // 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 33 | // 方法是按如下所示使用“*”: : 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("3.0.2.0")] 36 | [assembly: AssemblyFileVersion("3.0.2.0")] 37 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/Watcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Timers; 8 | 9 | namespace N_m3u8DL_CLI 10 | { 11 | class Watcher 12 | { 13 | private string dir = string.Empty; 14 | private int total = 0; 15 | private static double totalDuration = 0; //总时长 16 | private int now = 0; 17 | private int partsCount = 0; 18 | FileSystemWatcher watcher = new FileSystemWatcher(); 19 | 20 | public int Total { get => total; set => total = value; } 21 | public int Now { get => now; set => now = value; } 22 | public int PartsCount { get => partsCount; set => partsCount = value; } 23 | public static double TotalDuration { get => totalDuration; set => totalDuration = value; } 24 | 25 | public Watcher(string Dir) 26 | { 27 | this.dir = Dir; 28 | } 29 | 30 | public void WatcherStrat() 31 | { 32 | for (int i = 0; i < PartsCount; i++) 33 | { 34 | Now += Global.GetFileCount(dir + "\\Part_" + i.ToString(DownloadManager.partsPadZero), ".ts"); 35 | } 36 | watcher.Path = dir; 37 | watcher.Filter = "*.ts"; 38 | watcher.IncludeSubdirectories = true; //包括子目录 39 | watcher.EnableRaisingEvents = true; //开启提交事件 40 | watcher.Created += new FileSystemEventHandler(OnCreated); 41 | watcher.Renamed += new RenamedEventHandler(OnCreated); 42 | watcher.Deleted += new FileSystemEventHandler(OnDeleted); 43 | } 44 | 45 | public void WatcherStop() 46 | { 47 | watcher.Dispose(); 48 | } 49 | 50 | private void OnCreated(object source, FileSystemEventArgs e) 51 | { 52 | if (Path.GetFileNameWithoutExtension(e.FullPath).StartsWith("Part")) 53 | return; 54 | Now++; 55 | if (Now > Total) 56 | { 57 | return; 58 | } 59 | //Console.Title = Now + " / " + Total; 60 | string downloadedSize = Global.FormatFileSize(DownloadManager.DownloadedSize); 61 | string estimatedSize = Global.FormatFileSize(DownloadManager.DownloadedSize * total / now); 62 | int padding = downloadedSize.Length > estimatedSize.Length ? downloadedSize.Length : estimatedSize.Length; 63 | DownloadManager.ToDoSize = (DownloadManager.DownloadedSize * total / now) - DownloadManager.DownloadedSize; 64 | string percent = (Convert.ToDouble(now) / Convert.ToDouble(total) * 100).ToString("0.00") + "%"; 65 | var print = "Progress: " + Now + "/" + Total 66 | + $" ({percent}) -- {downloadedSize.PadLeft(padding)}/{estimatedSize.PadRight(padding)}"; 67 | ProgressReporter.Report(print, ""); 68 | } 69 | 70 | private void OnRenamed(object source, RenamedEventArgs e) 71 | { 72 | if (Path.GetFileNameWithoutExtension(e.FullPath).StartsWith("Part")) 73 | return; 74 | Now++; 75 | if (Now > Total) 76 | { 77 | return; 78 | } 79 | //Console.Title = Now + " / " + Total; 80 | string downloadedSize = Global.FormatFileSize(DownloadManager.DownloadedSize); 81 | string estimatedSize = Global.FormatFileSize(DownloadManager.DownloadedSize * total / now); 82 | int padding = downloadedSize.Length > estimatedSize.Length ? downloadedSize.Length : estimatedSize.Length; 83 | DownloadManager.ToDoSize = (DownloadManager.DownloadedSize * total / now) - DownloadManager.DownloadedSize; 84 | string percent = (Convert.ToDouble(now) / Convert.ToDouble(total) * 100).ToString("0.00") + "%"; 85 | var print = "Progress: " + Now + "/" + Total 86 | + $" ({percent}) -- {downloadedSize.PadLeft(padding)}/{estimatedSize.PadRight(padding)}"; 87 | ProgressReporter.Report(print, ""); 88 | } 89 | 90 | private void OnDeleted(object source, FileSystemEventArgs e) 91 | { 92 | if (Path.GetFileNameWithoutExtension(e.FullPath).StartsWith("Part")) 93 | return; 94 | Now--; 95 | if (Now > Total) 96 | { 97 | return; 98 | } 99 | //Console.Title = Now + " / " + Total; 100 | string downloadedSize = Global.FormatFileSize(DownloadManager.DownloadedSize); 101 | string estimatedSize = Global.FormatFileSize(DownloadManager.DownloadedSize * total / now); 102 | int padding = downloadedSize.Length > estimatedSize.Length ? downloadedSize.Length : estimatedSize.Length; 103 | DownloadManager.ToDoSize = (DownloadManager.DownloadedSize * total / now) - DownloadManager.DownloadedSize; 104 | string percent = (Convert.ToDouble(now) / Convert.ToDouble(total) * 100).ToString("0.00") + "%"; 105 | var print = "Progress: " + Now + "/" + Total 106 | + $" ({percent}) -- {downloadedSize.PadLeft(padding)}/{estimatedSize.PadRight(padding)}"; 107 | ProgressReporter.Report(print, ""); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/changelog.txt: -------------------------------------------------------------------------------- 1 | 2018年12月3日 2 | - 通过监控文件夹的更改来营造下载进度 3 | - 增加对EXT-X-DISCONTINUITY的处理(分部分处理,分别合并) 4 | - 增加对腾讯视频HDR的支持(主要是EXT-X-MAP的处理) 5 | - 增加对Master List的支持(默认最高画质) 6 | - json文件UpdateTime属性值改为"o" 符合国际标准 7 | - 按照EXT-X-DISCONTINUITY划分出的视频组采用COPY /B方式合并,最后用ffmpeg concat合并为单一文件 8 | 2018年12月5日 9 | - 转换为.Net Core项目(放弃) 10 | 2018年12月10日 11 | - 修改M3u8Do中的多线程下载,改为线程局部变量 12 | 2018年12月11日 13 | - 修复BUG,处理拼接相对路径中含有冒号的情况 14 | 2018年12月13日 15 | - 读写锁机制确保LOG正确写入 16 | - 跳过优酷广告分片 17 | 2018年12月14日 18 | - 如果Parts不等于1,就强制转换到MPEGTS封装 19 | - 如果Parts不等于1,启动新线程合并 20 | - 优化点播直播的判断 21 | - 修复获取属性的BUG(由','分割字符串,codecs里也有','造成) 22 | - baseurl增加冒号的拼接逻辑 23 | 2018年12月17日 24 | - 支持本地m3u8+本地ts文件形式 25 | 2018年12月19日 26 | - 支持 EXT-X-BYTERANGE 标签(点播) 27 | 2018年12月25日 28 | - 修改判断直播与点播的逻辑 29 | - 优酷 默认修改为 drm_type=3&drm_device=10 30 | - HttpDownloadFileToBytes 支持解压Gzip压缩且不再依赖服务器返回的ContentLength(bug fixed) 31 | - CombineURL 改用 Uri 类来拼接baseurl和url,普适性更强,无脑截取丢给它也可以拼接出正确的地址(bug fixed) 32 | 2018年12月26日 33 | - 修复Bug,增加变量startIndex,使用 segIndex-startIndex 计算分段总数 34 | 2019年1月23日 35 | - parser规范化,使用Jobject构造Json文件(切记:使用 new 来清空对象,不要用Clear,否则会导致之前加入的对象被同时清空) 36 | - 使用WebClient下载,并优化m3u8的Range处理 37 | 2019年1月24日 38 | - 修复:在master列表检测时需重置Baseurl 39 | 2019年2月23日 40 | - 优化下载 41 | - 重试次数增加到5 42 | - 完成后不显示进度 43 | - 命令行支持自定义MuxFastStart 44 | 2019年3月8日 45 | - 重写对linetv的key分析,比对重复 46 | 2019年3月11日 47 | - 自动判断音轨决定是否加入-bsf:a aac_adtstoasc参数 48 | 2019年3月18日 49 | - 固定几行UI,可显示下载速度以及进度(计算文件夹大小实现) 50 | - 混流时寻找ddpAudio.txt里的杜比音轨路径,封装杜比音轨 51 | 2019年3月20日 52 | - 通过Global.ShouldStop变量,完成了速度为零3次的自动杀进程功能(HTTP写入流也强行结束) 53 | 2019年3月25日 54 | - 优化下载函数 55 | 2019年3月29日 56 | - 0:a? 57 | - 修复对#EXT-X-BYTERANGE的支持 58 | 2019年3月30日 59 | - 删除Remove()函数,改为在Global.HttpDownloadFile()执行该逻辑 60 | 2019年3月31日 61 | - Global.HttpDownloadFile()采用using包围 62 | - 找不到ffmpeg报异常 63 | - Log写入Command Line 64 | 2019年4月11日 65 | - 支持爱奇艺杜比视界,并判断如果是杜比视界则采用二进制合并 66 | - 暂时去掉分段检测TS封装 67 | 2019年4月12日 68 | - 最低16线程 最高32 69 | - 修复AAC滤镜识别 70 | - 支持腾讯视频杜比视界 71 | 2019年4月13日 72 | - 增加downLen和totalLen对比是否下载完全 73 | 2019年4月18日 74 | - 命令行模式正式化,发布1.0版本 75 | 2019年4月24日 76 | - 增加enableBinaryMerge选项 77 | - 修复Bug 78 | 2019年4月30日 79 | - 增加仅解析功能 --enableParseOnly 80 | - 支持从已解析的meta.json文件中直接进行下载 81 | 2019年5月3日 82 | - 可下载纯音频m3u8 83 | 2019年5月6日 84 | - 修改速度计算方式(增加BYTE) 85 | - 修复ContentLength引发的BUG 86 | 2019年6月5日 87 | - 外部ddp逻辑优化 88 | - 跳过已存在文件时防止被速度监控程序杀死 89 | - 增加过多分片(>1800)合并逻辑 90 | 2019年6月6日 91 | - 支持DMM视频网站m3u8下载 92 | - 增加全局异常捕获 93 | - ffmpeg合并时去掉-map 0:d,因为mp4容器不支持此类数据 94 | 2019年6月7日 95 | - 支持删除混流的日期参数 96 | 2019年6月8日 97 | - 通过request.ReadWriteTimeout解决不能及时重试的问题 98 | - 下载失败后不会卡在按任意键继续 99 | - 添加timeout参数 100 | 2019年6月9日 101 | - 过滤m3u8内容中的空白行 102 | - 修复BUG(不该验证Status=200) 103 | - 增加显示更多信息(百分比/已下载/估计大小/估计时长) 104 | - 增加对优酷杜比视界的支持 105 | - 优化判断杜比视界的逻辑 106 | 2019年6月10日 107 | - 获取文件时排序,防止在网络驱动器中的致命BUG 108 | - AllowAutoRedirect = true 去掉Get302函数 109 | - 解决XP系统低版本.net框架的一个URL拼接bug 110 | - 为兼容XP系统 使用Environment.SetEnvironmentVariable替代了StartInfo.Environment 111 | - 修复获取属性值的一个bug 112 | 2019年6月12日 113 | - 自动下载m3u8外挂音轨、字幕等 114 | 2019年6月14日 115 | - 自动处理芒果TV请求头 116 | 2019年6月16日 117 | - 为兼容XP做出调整(https安全协议 SecurityProtocol) 118 | 2019年6月17日 119 | - 修复同名覆盖的BUG 120 | - LOG写入正确的工作目录 121 | - 修复下载额外字幕、音频时未能继承ReqHeaders的问题 122 | 2019年6月18日 123 | - 添加图标 124 | - 增加程序更新检测 125 | 2019年6月19日 126 | - 修复升级BUG 127 | - 自动下载更新 128 | 2019年6月23日 129 | - LOG写入到程序EXE所在目录 130 | - 环境变量检测BUG修复 131 | 2019年7月7日 132 | - 芒果自动加Cookie 133 | - 支持分段形式伪m3u8的正确合并 134 | 2019年7月8日 135 | - 修改默认UA为 VLC/2.2.1 LibVLC/2.2.1 136 | 2019年7月10日 137 | - 支持气球云m3u8 138 | 2019年7月10日 139 | - 修复获取属性值的BUG 140 | 2019年7月10日 141 | - 支持阿里云大学m3u8 142 | 2019年7月23日 143 | - 在TS格式检测中放行杜比视界视频 144 | - 自动去除优酷视频的广告(当指定downloadRange时不会启动) 145 | - 支持手动指定想要下载的内容(downloadRange) 146 | 2019年7月29日 147 | - 自动修改为爱奇艺UA 148 | 2019年8月21日 149 | - 增加originalCount属性,修复选取时间段后可能导致的合并顺序错乱问题 150 | - 增加noMerge命令行参数 151 | - 增加noProxy命令行参数 152 | 2019年8月22日 153 | - 增加stopSpeed命令行参数 154 | - Invalid Url至多提示20次 155 | 2019年8月28日 156 | - 优化腾讯杜比视界的识别 157 | 2019年9月5日 158 | - 更改输出信息,输出显示更多下载细节 159 | - 可以识别单音轨,自动合并为指定格式 160 | - 支持双击后输入命令 161 | - 避免重试时再次检测视频 162 | - 识别MPEG-TS封装时略过纯音频 163 | - 会首先下载第一个分片用以读取信息 164 | - 修复302状态码Baseurl错误的问题 165 | - 修正流匹配的正则表达式 166 | 2019年9月8日 167 | - 修复视频被识别为音频的BUG 168 | 2019年9月9日 169 | - 如果Parts大于1,则强制进行MPEG-TS封装 170 | - 修改Parts大于1时的下载逻辑,提升下载速度 171 | 2019年9月10日 172 | - 修改读取视频信息的逻辑 173 | - 优化直播下载的信息输出 174 | 2019年9月16日 175 | - 修复下载外挂流时显示异常问题 176 | 2019年9月18日 177 | - 每秒计算一次速度 178 | - 下载首分片将不触发停速重试 179 | - 加入全局限速功能 180 | 2019年9月27日 181 | - 支持www.vlive.tv 182 | 2019年10月5日 183 | - N_m3u8DL-CLI.args.txt 184 | - 细节优化 185 | 2019年10月18日 186 | - 去掉了优酷DRM设备参数更改 187 | 2019年10月23日 188 | - 增加disableIntegrityCheck选项 189 | 2019年10月24日 190 | - 捕获Ctrl+C退出,移动光标到正确位置 191 | 2019年11月30日 192 | - 完善芒果TV请求头的自动添加 193 | 2019年12月16日 194 | - 处理文件名特殊字符 195 | 2019年12月18日 196 | - 修复m3u8解析bug导致的无法合并问题 197 | - 增加杜比视界识别场景 198 | - 修复part大于1时读取json混流文件的严重错误 199 | - 自动去除优酷的广告分片及前情提要 200 | - 修复腾讯视频HDR10视频下载合并异常问题 201 | 2020年1月26日 202 | - 在央视频回看链接且有endtime参数的情况下,不识别为直播流 203 | 2020年1月29日 204 | - 修复识别大师列表的bug (多个字幕同一个GROUP-ID) 205 | - 修复vtt字幕无法正常合并的bug 206 | 2020年1月31日 207 | - ?__gda__行为优化 208 | 2020年2月1日 209 | - 修复bug 210 | - 支援twitcasting下载 211 | 2020年2月3日 212 | - 解密异常则退出程序 213 | - 通过json下载时若已存在文件则覆盖 214 | 2020年2月18日 215 | - 修正获取BaseUrl的BUG 216 | - 重新打包dll 217 | 2020年2月23日 218 | - 不支持的加密方式将标记为NOTSUPPORTED并强制启用二进制合并 219 | - 启用二进制合并的情况下,如果m3u8文件中存在map文件,则合并为mp4格式 220 | 2020年2月24日 221 | - 直播流录制优化逻辑,避免忙等待 222 | - 直播Waiting时,不再输出Parser内容 223 | - 直播录制的日志记录 224 | - 增加新的选项--liveRecDur限制直播录制时长 225 | 2020年2月27日 226 | - 细节bug修复 227 | 2020年2月28日 228 | - 修复本地masterList的读取问题 229 | - 在程序目录下创建NO_UPDATE文件可以禁止启动时检测更新 230 | 2020年2月29日 231 | - 识别#EXT-X-TARGETDURATION时,支持非整数 232 | 2020年3月2日 233 | - 支持51cto的key自动解密 234 | - 请求m3u8内容时,有10次自动重试 235 | - 直播下载自动设置请求分段文件时间间隔 236 | - 修复网络断线一直Downloading及cpu 100% 237 | - 加入savename参数仍可读取N_m3u8DL-CLI.args.txt 238 | - 直播下载跳过响应码为400的片段 239 | 2020年3月3日 240 | - 修复输出太长只在最后一行显示的问题 241 | 2020年3月4日 242 | - 只认第一个"#EXT-X-MAP", 其余的全部丢弃 243 | - 逻辑优化 244 | 2020年3月5日 245 | - 增加同名文件合并时共存策略 246 | 2020年4月17日 247 | - 优化异常捕获 248 | - 细节优化 249 | 2020年4月22日 250 | - 51cto getsign 251 | 2020年5月23日 252 | - 优酷杜比视界下载逻辑优化 253 | 2020年6月15日 254 | - 支持IMOCO m3u8/key解密 255 | 2020年7月18日 256 | - 从当前路径和exe路径同时寻找ffmpeg 257 | - 支持多语言本地化(简繁英) 258 | 2020年8月4日 259 | - 修复外挂字幕命名问题 260 | - 修复外挂字幕识别问题 261 | - 修复外挂轨道的一些逻辑问题 262 | - 优化多语言识别逻辑 263 | 2020年8月5日 264 | - 支持相对时间的vtt合并(还存在问题) 265 | 2020年8月9日 266 | - 修复IV错误导致的AES-128解密异常问题 267 | - 支持自定义IV(--useKeyIV) 268 | 2020年9月12日 269 | - 支持nfmovies m3u8解密 270 | - 支持自动去除PNG Header(https://puui.qpic.cn/newsapp_ls/0/12418116195/0) 271 | - 修复相对时间的vtt合并的一些错误逻辑(还存在问题) 272 | 2020年9月19日 273 | - 在自定义KEY且未自定义IV情况下,自动读取m3u8中存在的IV 274 | - 支持阿房影视等ddyun m3u8解密 275 | 2020年10月14日 276 | - 咪咕分片链接后拼接m3u8_url参数 277 | - 修复文件名过长导致的BUG 278 | - 优化ffmpeg调用逻辑 279 | 2020年11月17日 280 | - 将默认UA修改为 Mozilla/5.0 (Linux; U; Android 7.0; zh-cn; 15 Plus Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/9.4 Mobile Safari/537.36 281 | - m3u8响应长度大于50M则丢弃 282 | - 修正使用AAC滤镜的逻辑 283 | - 识别EXT-X-PROGRAM-DATE-TIME 284 | 2020年11月20日 285 | - 识别大部分mpd地址,自动转换为m3u8并下载 286 | - GIF HEADER检测 287 | - 修复BUG 288 | 2020年11月22日 289 | - 解决HTTPS协议自动重定向后,Referer丢失问题 290 | - 新的任务速度监控逻辑 291 | 2020年11月23日 292 | - 将默认UA修改为 Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36 293 | - 修改芒果TV请求头 294 | 2020年11月25日 295 | - 修正MPD判断最高清晰度的逻辑 296 | - 在MPD输入下支持选择音轨 297 | - 修复BUG 298 | 2020年11月26日 299 | - 优化MPD识别方案 300 | - 修复MPD情况下时间戳溢出问题 301 | 2020年12月2日 302 | - FIX Language Bug 303 | 2020年12月6日 304 | - 使用手机UA请求气球云密钥服务器 305 | 2020年12月12日 306 | - 修复MPD下同一个ID分散在不同Period导致下载不完全问题 307 | 2020年12月20日 308 | - 支持解密虎课网 309 | 2021年1月18日 310 | - 完善MPD下载相关 311 | - 重新打包多语言资源 312 | 2021年1月24日 313 | - 适配Disney+资源 314 | - MPD选择流行为优化 315 | - 修复二进制合并时vtt字幕被合并为ts后缀问题 316 | 2021年2月1日 317 | - 修正自定义KEY且存在IV时的隐患 318 | - 优化跳过PNG Header的算法 319 | 2021年2月2日 320 | - 独播库自动加入referer 321 | - 修复气球云 322 | 2021年2月10日 323 | - 修正MPD拼接BaseUrl逻辑 324 | 2021年2月11日 325 | - 将CNTV视频修改为未加密链接 326 | 2021年2月21日 327 | - MPD检测最后一个分片是否有效 328 | 2021年2月22日 329 | - 添加用户网络代理支持,使用--proxyAddress指定代理地址。(@evanlabs) 330 | 2021年3月3日 331 | - 修复M3U8选择音轨/字幕不生效问题 332 | - 外挂音轨时enableAudioOnly可仅下载音频 333 | - 移除气球云支持 334 | 2021年3月15日 335 | - 修复enableAudioOnly且下载MPD文件时留下冗余(Audio)文件夹的情况 336 | 2021年3月22日 337 | - 适配AppleTv资源 338 | 2021年3月25日 339 | - 优化下载监控 340 | - 为下载分片增加了自动重试机制(3次) 341 | 2021年3月27日 342 | - 优化显示输出 343 | - 增加ETA显示 344 | 2021年6月27日 345 | - 修正判断png图片时可能出现的数组越界bug 346 | - 支持解压brotli(测试地址 https://www.baobuzz.com/m3u8/236963.m3u8?sign=811ae52382b7dd1d247f705e1bcaddf4) 347 | 2021年7月4日 348 | - 优化master选择最高清晰度逻辑(大于改为大于等于) 349 | - 支持爱奇艺DRM-JSON自动转换为m3u8 350 | 2021年8月15日 351 | - 优化显示输出 352 | - 强校验MAP下载成功 353 | 2021年9月5日 354 | - 修复MPD节点选择BUG 355 | - 修复速度输出padding负值问题 356 | - 修复同一个period且同id导致被重复添加分片 357 | - 优化AppleTV判断 358 | 2021年10月19日 359 | - 修复选择清晰度在输入选项后界面异常问题 360 | - 修复日志冲突问题 361 | 2021年11月12日 362 | - 修复init url缺失baseurl问题 -------------------------------------------------------------------------------- /N_m3u8DL-CLI/logo_3Iv_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/N_m3u8DL-CLI/logo_3Iv_icon.ico -------------------------------------------------------------------------------- /N_m3u8DL-CLI/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /N_m3u8DL-CLI/strings.en-US.Designer.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/N_m3u8DL-CLI/strings.en-US.Designer.cs -------------------------------------------------------------------------------- /N_m3u8DL-CLI/strings.zh-TW.Designer.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/N_m3u8DL-CLI/strings.zh-TW.Designer.cs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | ███╗ ██╗ ███╗ ███╗██████╗ ██╗ ██╗ █████╗ ██████╗ ██╗ ██████╗██╗ ██╗ 4 | ████╗ ██║ ████╗ ████║╚════██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔════╝██║ ██║ 5 | ██╔██╗ ██║ ██╔████╔██║ █████╔╝██║ ██║╚█████╔╝██║ ██║██║█████╗██║ ██║ ██║ 6 | ██║╚██╗██║ ██║╚██╔╝██║ ╚═══██╗██║ ██║██╔══██╗██║ ██║██║╚════╝██║ ██║ ██║ 7 | ██║ ╚████║███████╗██║ ╚═╝ ██║██████╔╝╚██████╔╝╚█████╔╝██████╔╝███████╗ ╚██████╗███████╗██║ 8 | ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚════╝ ╚═════╝ ╚══════╝ ╚═════╝╚══════╝╚═╝ 9 | 10 | ``` 11 | --- 12 | [![img](https://img.shields.io/github/stars/nilaoda/N_m3u8DL-CLI?label=%E7%82%B9%E8%B5%9E)](https://github.com/nilaoda/N_m3u8DL-CLI) [![img](https://img.shields.io/github/last-commit/nilaoda/N_m3u8DL-CLI?label=%E6%9C%80%E8%BF%91%E6%8F%90%E4%BA%A4)](https://github.com/nilaoda/N_m3u8DL-CLI) [![img](https://img.shields.io/github/release/nilaoda/N_m3u8DL-CLI?label=%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)](https://github.com/nilaoda/N_m3u8DL-CLI/releases) [![img](https://img.shields.io/github/license/nilaoda/N_m3u8DL-CLI?label=%E8%AE%B8%E5%8F%AF%E8%AF%81)](https://github.com/nilaoda/N_m3u8DL-CLI) [![img](https://img.shields.io/badge/URL-%E7%94%A8%E6%88%B7%E6%96%87%E6%A1%A3-blue)](https://nilaoda.github.io/N_m3u8DL-CLI/) 13 | 14 | 15 | # [ENGLISH VERSION](https://github.com/nilaoda/N_m3u8DL-CLI/blob/master/README_ENG.md) 16 | 17 | # 下载使用 18 | * 发行版: https://github.com/nilaoda/N_m3u8DL-CLI/releases 19 | * 自动构建版`(供测试)`: https://github.com/nilaoda/N_m3u8DL-CLI/actions 20 | 21 | # 关于开源 22 | 本项目已于2019年10月9日开源,采用MIT许可证,各取所需。 23 | 24 | # 关于跨平台 25 | * N_m3u8DL-CLI `(本项目)`: 基于 .NET Framework, 不具备跨平台能力. 目前已进入维护阶段. 26 | 27 | * [N_m3u8DL-RE](https://github.com/nilaoda/N_m3u8DL-RE) : 抛弃历史包袱从零做起, 支持Win/Linux/Mac, 更丰富的功能会在这里出现 ... 28 | 29 | # N_m3u8DL-CLI 30 | 一个**简单易用的**m3u8下载器,下载地址:https://github.com/nilaoda/N_m3u8DL-CLI/releases 31 | 32 | 支持下载m3u8链接或文件为`mp4`或`ts`格式,并提供丰富的命令行选项。 33 | * **不支持**优酷视频解密 34 | * **不支持**气球云视频解密 35 | * 支持`AES-128-CBC`加密自动解密 36 | * 支持多线程下载 37 | * 支持下载限速 38 | * 支持断点续传 39 | * 支持`Master List` 40 | * 支持直播流录制(`BETA`) 41 | * 支持自定义`HTTP Headers` 42 | * 支持自动合并 (二进制合并或使用ffmpeg合并) 43 | * 支持选择下载`m3u8`中的指定时间段/分片内容 44 | * 支持下载路径为网络驱动器的情况 45 | * 支持下载外挂字幕轨道、音频轨道 46 | * 支持仅合并为音频 47 | * 支持设置特定http代理 48 | * 支持自动使用系统代理(默认行为, 可禁止) 49 | * 支持m3u8dl链接协议(通过web链接调用本机客户端) 50 | * 提供SimpleG简易的`GUI`生成常用参数 51 | 52 | 53 | 54 | ![运行截图](https://nilaoda.github.io/N_m3u8DL-CLI/source/images/%E7%9B%B4%E6%8E%A5%E4%BD%BF%E7%94%A8.gif) 55 | 56 | # 命令行选项 57 | ``` 58 | N_m3u8DL-CLI 59 | 60 | USAGE: 61 | 62 | N_m3u8DL-CLI [OPTIONS] 63 | 64 | OPTIONS: 65 | 66 | --workDir 设定程序工作目录 67 | --saveName 设定存储文件名(不包括后缀) 68 | --baseUrl 设定Baseurl 69 | --headers 设定请求头,格式 key:value 使用|分割不同的key&value 70 | --maxThreads (Default: 32) 设定程序的最大线程数 71 | --minThreads (Default: 16) 设定程序的最小线程数 72 | --retryCount (Default: 15) 设定程序的重试次数 73 | --timeOut (Default: 10) 设定程序网络请求的超时时间(单位为秒) 74 | --muxSetJson 使用外部json文件定义混流选项 75 | --useKeyFile 使用外部16字节文件定义AES-128解密KEY 76 | --useKeyBase64 使用Base64字符串定义AES-128解密KEY 77 | --useKeyIV 使用HEX字符串定义AES-128解密IV 78 | --downloadRange 仅下载视频的一部分分片或长度 79 | --liveRecDur 直播录制时,达到此长度自动退出软件(HH:MM:SS) 80 | --stopSpeed 当速度低于此值时,重试(单位为KB/s) 81 | --maxSpeed 设置下载速度上限(单位为KB/s) 82 | --proxyAddress 设置HTTP/SOCKS5代理, 如 http://127.0.0.1:8080 83 | --enableDelAfterDone 开启下载后删除临时文件夹的功能 84 | --enableMuxFastStart 开启混流mp4的FastStart特性 85 | --enableBinaryMerge 开启二进制合并分片 86 | --enableParseOnly 开启仅解析模式(程序只进行到meta.json) 87 | --enableAudioOnly 合并时仅封装音频轨道 88 | --disableDateInfo 关闭混流中的日期写入 89 | --disableIntegrityCheck 不检测分片数量是否完整 90 | --noMerge 禁用自动合并 91 | --noProxy 不自动使用系统代理 92 | --registerUrlProtocol 注册m3u8dl链接协议 93 | --unregisterUrlProtocol 取消注册m3u8dl链接协议 94 | --enableChaCha20 enableChaCha20 95 | --chaCha20KeyBase64 ChaCha20KeyBase64 96 | --chaCha20NonceBase64 ChaCha20NonceBase64 97 | --help Display this help screen. 98 | --version Display version information. 99 | ``` 100 | 101 | # 关于`m3u8dl://`协议 102 | 新增命令行参数: 103 | ``` 104 | --registerUrlProtocol 注册m3u8dl链接协议 105 | --unregisterUrlProtocol 取消注册m3u8dl链接协议 106 | ``` 107 | 108 | URI格式: 109 | ``` 110 | m3u8dl:// 111 | ``` 112 | 113 | URI示例: 114 | ``` 115 | m3u8dl://Imh0dHBzOi8vZXhhbXBsZS5jb20vYWJjLm0zdTgiIC0td29ya0RpciAiJVVTRVJQUk9GSUxFJVxEb3dubG9hZHNcbTN1OGRsIiAtLXNhdmVOYW1lICJhYmMiIC0tZW5hYmxlRGVsQWZ0ZXJEb25lIC0tZGlzYWJsZURhdGVJbmZvIC0tbm9Qcm94eQ== 116 | ``` 117 | 118 | URI解码结果: 119 | ``` 120 | "https://example.com/abc.m3u8" --workDir "%USERPROFILE%\Downloads\m3u8dl" --saveName "abc" --enableDelAfterDone --disableDateInfo --noProxy 121 | ``` 122 | 123 | # 用户文档 124 | https://nilaoda.github.io/N_m3u8DL-CLI/ 125 | 126 | # 聊聊 127 | https://discord.gg/SSGwKrjC44 128 | 129 | # 赞赏 130 | ![Wow](https://nilaoda.github.io/N_m3u8DL-CLI/source/images/alipay.png) 131 | -------------------------------------------------------------------------------- /README_ENG.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | ███╗ ██╗ ███╗ ███╗██████╗ ██╗ ██╗ █████╗ ██████╗ ██╗ ██████╗██╗ ██╗ 4 | ████╗ ██║ ████╗ ████║╚════██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔════╝██║ ██║ 5 | ██╔██╗ ██║ ██╔████╔██║ █████╔╝██║ ██║╚█████╔╝██║ ██║██║█████╗██║ ██║ ██║ 6 | ██║╚██╗██║ ██║╚██╔╝██║ ╚═══██╗██║ ██║██╔══██╗██║ ██║██║╚════╝██║ ██║ ██║ 7 | ██║ ╚████║███████╗██║ ╚═╝ ██║██████╔╝╚██████╔╝╚█████╔╝██████╔╝███████╗ ╚██████╗███████╗██║ 8 | ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚════╝ ╚═════╝ ╚══════╝ ╚═════╝╚══════╝╚═╝ 9 | 10 | ``` 11 | This is an m3u8 downloader. 12 | ## Summary 13 | Supports: 14 | * Auto decrypt for `AES-128-CBC` 15 | * `Master List` 16 | * Live stream recording(`BETA`) 17 | * Customize HTTP headers 18 | * Auto merge clips(Binary or ffmpeg) 19 | * Select save clip by `time code` or `index` 20 | * Network driver on Windows OS 21 | * Alternative audio/video track 22 | * Mux without video track 23 | * Custom HTTP proxy or Use system proxy 24 | * Optimization for Chinese streaming platforms 25 | 26 | ![ScreenShot](https://nilaoda.github.io/N_m3u8DL-CLI/source/images/%E7%9B%B4%E6%8E%A5%E4%BD%BF%E7%94%A8.gif) 27 | 28 | ## GUI 29 | * Easy-to-use `GUI` 30 | 31 | ## Options 32 | ``` 33 | N_m3u8DL-CLI 34 | 35 | USAGE: 36 | 37 | N_m3u8DL-CLI [OPTIONS] 38 | 39 | OPTIONS: 40 | 41 | --workDir Set work dir (Video will be here) 42 | --saveName Set save name(Exclude extention) 43 | --baseUrl Set Baseurl 44 | --headers Set HTTP headers,format: key:value use | split all 45 | key&value 46 | --maxThreads (Default: 32) Set max thread 47 | --minThreads (Default: 16) Set min thread 48 | --retryCount (Default: 15) Set retry times 49 | --timeOut (Default: 10) Set timeout for http request(second) 50 | --muxSetJson Set a json file for mux 51 | --useKeyFile Use 16 bytes file as KEY for AES-128 decryption 52 | --useKeyBase64 Use Base64 String as KEY for AES-128 decryption 53 | --useKeyIV Use HEX String as IV for AES-128 decryption 54 | --downloadRange Set range for a video 55 | --liveRecDur When the live recording reaches this length, the 56 | software will exit automatically(HH:MM:SS) 57 | --stopSpeed Speed below this, retry(KB/s) 58 | --maxSpeed Set max download speed(KB/s) 59 | --proxyAddress Set HTTP/SOCKS5 Proxy, like http://127.0.0.1:8080 60 | --enableDelAfterDone Enable delete clips after download completed 61 | --enableMuxFastStart Enable fast start for mp4 62 | --enableBinaryMerge Enable use binary merge instead of ffmpeg 63 | --enableParseOnly Enable parse only mode 64 | --enableAudioOnly Enable only audio track when mux use ffmpeg 65 | --disableDateInfo Disable write date info when mux use ffmpeg 66 | --disableIntegrityCheck Disable integrity check 67 | --noMerge Disable auto merge 68 | --noProxy Disable use system proxy 69 | --registerUrlProtocol Register m3u8dl URL protocol 70 | --unregisterUrlProtocol Unregister m3u8dl URL protocol 71 | --enableChaCha20 enableChaCha20 72 | --chaCha20KeyBase64 ChaCha20KeyBase64 73 | --chaCha20NonceBase64 ChaCha20NonceBase64 74 | --help Display this help screen. 75 | --version Display version information. 76 | ``` 77 | 78 | ## About `m3u8dl://` 79 | New commandline options: 80 | ``` 81 | --registerUrlProtocol Register m3u8dl URL protocol 82 | --unregisterUrlProtocol Unregister m3u8dl URL protocol 83 | ``` 84 | 85 | URI Format: 86 | ``` 87 | m3u8dl:// 88 | ``` 89 | 90 | URI Example: 91 | ``` 92 | m3u8dl://Imh0dHBzOi8vZXhhbXBsZS5jb20vYWJjLm0zdTgiIC0td29ya0RpciAiJVVTRVJQUk9GSUxFJVxEb3dubG9hZHNcbTN1OGRsIiAtLXNhdmVOYW1lICJhYmMiIC0tZW5hYmxlRGVsQWZ0ZXJEb25lIC0tZGlzYWJsZURhdGVJbmZvIC0tbm9Qcm94eQ== 93 | ``` 94 | 95 | URI Decode Result: 96 | ``` 97 | "https://example.com/abc.m3u8" --workDir "%USERPROFILE%\Downloads\m3u8dl" --saveName "abc" --enableDelAfterDone --disableDateInfo --noProxy 98 | ``` 99 | 100 | ## Document 101 | https://nilaoda.github.io/N_m3u8DL-CLI/ 102 | 103 | ## Chit-chat 104 | https://discord.gg/RscAJZv3Yq 105 | -------------------------------------------------------------------------------- /docs/GetM3u8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JS获取m3u8 · N_m3u8DL-CLI文档 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 76 | 79 | 80 | 81 | 82 | 83 |
84 |
85 | 86 | 87 | 90 | 91 | 92 | 295 | 296 | 297 |
298 | 299 |
300 | 301 |
302 | 303 | 304 | 305 | 314 | 315 | 316 | 317 | 318 |
319 |
320 | 321 |
322 |
323 | 324 |
325 | 326 |

使用Javascript获取m3u8

327 |

这里变得空空如也...

328 | 329 | 330 |
331 | 332 |
333 |
334 |
335 | 336 |

results matching ""

337 |
    338 | 339 |
    340 |
    341 | 342 |

    No results matching ""

    343 | 344 |
    345 |
    346 |
    347 | 348 |
    349 |
    350 | 351 |
    352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 |
    363 | 364 | 370 |
    371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | -------------------------------------------------------------------------------- /docs/Introductory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 使用入门 · N_m3u8DL-CLI文档 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | 77 | 78 | 79 | 80 | 81 |
    82 |
    83 | 84 | 85 | 88 | 89 | 90 | 293 | 294 | 295 |
    296 | 297 |
    298 | 299 |
    300 | 301 | 302 | 303 | 312 | 313 | 314 | 315 | 316 |
    317 |
    318 | 319 |
    320 |
    321 | 322 |
    323 | 324 |

    让我们开始吧

    325 |

    首先,最简单的使用方式是直接双击EXE,将你要下载的m3u8文件或m3u8链接复制进去,然后按下回车键。就像这样:
    直接使用
    正常情况下,程序将产生如下的目录结构:

    326 |
    .
    327 | ├── Downloads
    328 | └── Logs
    329 |     └── *.log
    330 | 

    程序默认将视频文件放在了EXE同目录的Downloads文件夹中,将程序运行日志信息放在了Logs目录中。

    331 | 332 | 333 |
    334 | 335 |
    336 |
    337 |
    338 | 339 |

    results matching ""

    340 |
      341 | 342 |
      343 |
      344 | 345 |

      No results matching ""

      346 | 347 |
      348 |
      349 |
      350 | 351 |
      352 |
      353 | 354 |
      355 | 356 | 357 | 358 | 359 | 360 | 361 |
      362 | 363 | 369 |
      370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | -------------------------------------------------------------------------------- /docs/M3U8URL2File.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | M3U8URL2File · N_m3u8DL-CLI文档 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 76 | 79 | 80 | 81 | 82 | 83 |
      84 |
      85 | 86 | 87 | 90 | 91 | 92 | 295 | 296 | 297 |
      298 | 299 |
      300 | 301 |
      302 | 303 | 304 | 305 | 314 | 315 | 316 | 317 | 318 |
      319 |
      320 | 321 |
      322 |
      323 | 324 |
      325 | 326 |

      M3U8URL2File

      327 |

      一款帮助你将m3u8链接下载为m3u8文件的小软件。
      https://github.com/nilaoda/M3U8URL2File/releases

      328 |

      程序界面

      329 |

      程序界面

      330 | 331 | 332 |
      333 | 334 |
      335 |
      336 |
      337 | 338 |

      results matching ""

      339 |
        340 | 341 |
        342 |
        343 | 344 |

        No results matching ""

        345 | 346 |
        347 |
        348 |
        349 | 350 |
        351 |
        352 | 353 |
        354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
        365 | 366 | 372 |
        373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | -------------------------------------------------------------------------------- /docs/gitbook/fonts/fontawesome/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/fonts/fontawesome/FontAwesome.otf -------------------------------------------------------------------------------- /docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/gitbook/fonts/fontawesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/fonts/fontawesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-donate/plugin.css: -------------------------------------------------------------------------------- 1 | .gitbook-donate { 2 | padding: 10px 0; margin: 20px auto; width: 90%; text-align: center; 3 | } 4 | 5 | #rewardButton { 6 | cursor: pointer; 7 | border: 0; 8 | outline: 0; 9 | border-radius: 100%; 10 | padding: 0; 11 | margin: 0; 12 | letter-spacing: normal; 13 | text-transform: none; 14 | text-indent: 0px; 15 | text-shadow: none; 16 | } 17 | #rewardButton span { 18 | display: inline-block; 19 | width: 80px; 20 | height: 35px; 21 | line-height: 35px; 22 | border-radius: 5px; 23 | color: #fff; 24 | font-weight: 400; 25 | font-style: normal; 26 | font-variant: normal; 27 | font-stretch: normal; 28 | font-size: 18px; 29 | font-family: "Microsoft Yahei"; 30 | background: #f44336; 31 | } 32 | #rewardButton span:hover { 33 | background: #f7877f; 34 | } 35 | #QR { 36 | padding-top: 20px; 37 | } 38 | #QR a { 39 | border: 0; 40 | } 41 | #QR img { 42 | width: 180px; 43 | max-width: 100%; 44 | display: inline-block; 45 | margin: 0.8em 2em 0 2em; 46 | } 47 | #wechat:hover p { 48 | animation: roll 0.1s infinite linear; 49 | -webkit-animation: roll 0.1s infinite linear; 50 | -moz-animation: roll 0.1s infinite linear; 51 | } 52 | #alipay:hover p { 53 | animation: roll 0.1s infinite linear; 54 | -webkit-animation: roll 0.1s infinite linear; 55 | -moz-animation: roll 0.1s infinite linear; 56 | } 57 | @-moz-keyframes roll { 58 | from { 59 | -webkit-transform: rotateZ(30deg); 60 | -moz-transform: rotateZ(30deg); 61 | -ms-transform: rotateZ(30deg); 62 | -o-transform: rotateZ(30deg); 63 | transform: rotateZ(30deg); 64 | } 65 | to { 66 | -webkit-transform: rotateZ(-30deg); 67 | -moz-transform: rotateZ(-30deg); 68 | -ms-transform: rotateZ(-30deg); 69 | -o-transform: rotateZ(-30deg); 70 | transform: rotateZ(-30deg); 71 | } 72 | } 73 | @-webkit-keyframes roll { 74 | from { 75 | -webkit-transform: rotateZ(30deg); 76 | -moz-transform: rotateZ(30deg); 77 | -ms-transform: rotateZ(30deg); 78 | -o-transform: rotateZ(30deg); 79 | transform: rotateZ(30deg); 80 | } 81 | to { 82 | -webkit-transform: rotateZ(-30deg); 83 | -moz-transform: rotateZ(-30deg); 84 | -ms-transform: rotateZ(-30deg); 85 | -o-transform: rotateZ(-30deg); 86 | transform: rotateZ(-30deg); 87 | } 88 | } 89 | @-o-keyframes roll { 90 | from { 91 | -webkit-transform: rotateZ(30deg); 92 | -moz-transform: rotateZ(30deg); 93 | -ms-transform: rotateZ(30deg); 94 | -o-transform: rotateZ(30deg); 95 | transform: rotateZ(30deg); 96 | } 97 | to { 98 | -webkit-transform: rotateZ(-30deg); 99 | -moz-transform: rotateZ(-30deg); 100 | -ms-transform: rotateZ(-30deg); 101 | -o-transform: rotateZ(-30deg); 102 | transform: rotateZ(-30deg); 103 | } 104 | } 105 | @keyframes roll { 106 | from { 107 | -webkit-transform: rotateZ(30deg); 108 | -moz-transform: rotateZ(30deg); 109 | -ms-transform: rotateZ(30deg); 110 | -o-transform: rotateZ(30deg); 111 | transform: rotateZ(30deg); 112 | } 113 | to { 114 | -webkit-transform: rotateZ(-30deg); 115 | -moz-transform: rotateZ(-30deg); 116 | -ms-transform: rotateZ(-30deg); 117 | -o-transform: rotateZ(-30deg); 118 | transform: rotateZ(-30deg); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-donate/plugin.js: -------------------------------------------------------------------------------- 1 | require(['gitbook', 'jQuery'], function(gitbook, $) { 2 | var wechatURL; 3 | var alipayURL; 4 | var titleText; 5 | var buttonText; 6 | var wechatText; 7 | var alipayText; 8 | 9 | function insertDonateLink() { 10 | if ($('.gitbook-donate').length === 0 && wechatURL !== undefined && (wechatURL !== '' || alipayURL !== '')) { 11 | var html = [ 12 | '
        ', 13 | '
        ' + titleText + '
        ', 14 | '', 17 | '', '
        ']); 39 | $('.page-inner section.normal:last').after(html.join('')); 40 | } 41 | } 42 | 43 | gitbook.events.bind('start', function(e, config) { 44 | wechatURL = config.donate.wechat || ''; 45 | wechatText = config.donate.wechatText || '微信捐赠'; 46 | alipayURL = config.donate.alipay || ''; 47 | alipayText = config.donate.alipayText || '支付宝捐赠'; 48 | titleText = config.donate.title || ''; 49 | buttonText = config.donate.button || '赏'; 50 | insertDonateLink(); 51 | }); 52 | 53 | gitbook.events.bind('page.change', function() { 54 | insertDonateLink(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-fontsettings/fontsettings.js: -------------------------------------------------------------------------------- 1 | require(['gitbook', 'jquery'], function(gitbook, $) { 2 | // Configuration 3 | var MAX_SIZE = 4, 4 | MIN_SIZE = 0, 5 | BUTTON_ID; 6 | 7 | // Current fontsettings state 8 | var fontState; 9 | 10 | // Default themes 11 | var THEMES = [ 12 | { 13 | config: 'white', 14 | text: 'White', 15 | id: 0 16 | }, 17 | { 18 | config: 'sepia', 19 | text: 'Sepia', 20 | id: 1 21 | }, 22 | { 23 | config: 'night', 24 | text: 'Night', 25 | id: 2 26 | } 27 | ]; 28 | 29 | // Default font families 30 | var FAMILIES = [ 31 | { 32 | config: 'serif', 33 | text: 'Serif', 34 | id: 0 35 | }, 36 | { 37 | config: 'sans', 38 | text: 'Sans', 39 | id: 1 40 | } 41 | ]; 42 | 43 | // Return configured themes 44 | function getThemes() { 45 | return THEMES; 46 | } 47 | 48 | // Modify configured themes 49 | function setThemes(themes) { 50 | THEMES = themes; 51 | updateButtons(); 52 | } 53 | 54 | // Return configured font families 55 | function getFamilies() { 56 | return FAMILIES; 57 | } 58 | 59 | // Modify configured font families 60 | function setFamilies(families) { 61 | FAMILIES = families; 62 | updateButtons(); 63 | } 64 | 65 | // Save current font settings 66 | function saveFontSettings() { 67 | gitbook.storage.set('fontState', fontState); 68 | update(); 69 | } 70 | 71 | // Increase font size 72 | function enlargeFontSize(e) { 73 | e.preventDefault(); 74 | if (fontState.size >= MAX_SIZE) return; 75 | 76 | fontState.size++; 77 | saveFontSettings(); 78 | } 79 | 80 | // Decrease font size 81 | function reduceFontSize(e) { 82 | e.preventDefault(); 83 | if (fontState.size <= MIN_SIZE) return; 84 | 85 | fontState.size--; 86 | saveFontSettings(); 87 | } 88 | 89 | // Change font family 90 | function changeFontFamily(configName, e) { 91 | if (e && e instanceof Event) { 92 | e.preventDefault(); 93 | } 94 | 95 | var familyId = getFontFamilyId(configName); 96 | fontState.family = familyId; 97 | saveFontSettings(); 98 | } 99 | 100 | // Change type of color theme 101 | function changeColorTheme(configName, e) { 102 | if (e && e instanceof Event) { 103 | e.preventDefault(); 104 | } 105 | 106 | var $book = gitbook.state.$book; 107 | 108 | // Remove currently applied color theme 109 | if (fontState.theme !== 0) 110 | $book.removeClass('color-theme-'+fontState.theme); 111 | 112 | // Set new color theme 113 | var themeId = getThemeId(configName); 114 | fontState.theme = themeId; 115 | if (fontState.theme !== 0) 116 | $book.addClass('color-theme-'+fontState.theme); 117 | 118 | saveFontSettings(); 119 | } 120 | 121 | // Return the correct id for a font-family config key 122 | // Default to first font-family 123 | function getFontFamilyId(configName) { 124 | // Search for plugin configured font family 125 | var configFamily = $.grep(FAMILIES, function(family) { 126 | return family.config == configName; 127 | })[0]; 128 | // Fallback to default font family 129 | return (!!configFamily)? configFamily.id : 0; 130 | } 131 | 132 | // Return the correct id for a theme config key 133 | // Default to first theme 134 | function getThemeId(configName) { 135 | // Search for plugin configured theme 136 | var configTheme = $.grep(THEMES, function(theme) { 137 | return theme.config == configName; 138 | })[0]; 139 | // Fallback to default theme 140 | return (!!configTheme)? configTheme.id : 0; 141 | } 142 | 143 | function update() { 144 | var $book = gitbook.state.$book; 145 | 146 | $('.font-settings .font-family-list li').removeClass('active'); 147 | $('.font-settings .font-family-list li:nth-child('+(fontState.family+1)+')').addClass('active'); 148 | 149 | $book[0].className = $book[0].className.replace(/\bfont-\S+/g, ''); 150 | $book.addClass('font-size-'+fontState.size); 151 | $book.addClass('font-family-'+fontState.family); 152 | 153 | if(fontState.theme !== 0) { 154 | $book[0].className = $book[0].className.replace(/\bcolor-theme-\S+/g, ''); 155 | $book.addClass('color-theme-'+fontState.theme); 156 | } 157 | } 158 | 159 | function init(config) { 160 | // Search for plugin configured font family 161 | var configFamily = getFontFamilyId(config.family), 162 | configTheme = getThemeId(config.theme); 163 | 164 | // Instantiate font state object 165 | fontState = gitbook.storage.get('fontState', { 166 | size: config.size || 2, 167 | family: configFamily, 168 | theme: configTheme 169 | }); 170 | 171 | update(); 172 | } 173 | 174 | function updateButtons() { 175 | // Remove existing fontsettings buttons 176 | if (!!BUTTON_ID) { 177 | gitbook.toolbar.removeButton(BUTTON_ID); 178 | } 179 | 180 | // Create buttons in toolbar 181 | BUTTON_ID = gitbook.toolbar.createButton({ 182 | icon: 'fa fa-font', 183 | label: 'Font Settings', 184 | className: 'font-settings', 185 | dropdown: [ 186 | [ 187 | { 188 | text: 'A', 189 | className: 'font-reduce', 190 | onClick: reduceFontSize 191 | }, 192 | { 193 | text: 'A', 194 | className: 'font-enlarge', 195 | onClick: enlargeFontSize 196 | } 197 | ], 198 | $.map(FAMILIES, function(family) { 199 | family.onClick = function(e) { 200 | return changeFontFamily(family.config, e); 201 | }; 202 | 203 | return family; 204 | }), 205 | $.map(THEMES, function(theme) { 206 | theme.onClick = function(e) { 207 | return changeColorTheme(theme.config, e); 208 | }; 209 | 210 | return theme; 211 | }) 212 | ] 213 | }); 214 | } 215 | 216 | // Init configuration at start 217 | gitbook.events.bind('start', function(e, config) { 218 | var opts = config.fontsettings; 219 | 220 | // Generate buttons at start 221 | updateButtons(); 222 | 223 | // Init current settings 224 | init(opts); 225 | }); 226 | 227 | // Expose API 228 | gitbook.fontsettings = { 229 | enlargeFontSize: enlargeFontSize, 230 | reduceFontSize: reduceFontSize, 231 | setTheme: changeColorTheme, 232 | setFamily: changeFontFamily, 233 | getThemes: getThemes, 234 | setThemes: setThemes, 235 | getFamilies: getFamilies, 236 | setFamilies: setFamilies 237 | }; 238 | }); 239 | 240 | 241 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-fontsettings/website.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Theme 1 3 | */ 4 | .color-theme-1 .dropdown-menu { 5 | background-color: #111111; 6 | border-color: #7e888b; 7 | } 8 | .color-theme-1 .dropdown-menu .dropdown-caret .caret-inner { 9 | border-bottom: 9px solid #111111; 10 | } 11 | .color-theme-1 .dropdown-menu .buttons { 12 | border-color: #7e888b; 13 | } 14 | .color-theme-1 .dropdown-menu .button { 15 | color: #afa790; 16 | } 17 | .color-theme-1 .dropdown-menu .button:hover { 18 | color: #73553c; 19 | } 20 | /* 21 | * Theme 2 22 | */ 23 | .color-theme-2 .dropdown-menu { 24 | background-color: #2d3143; 25 | border-color: #272a3a; 26 | } 27 | .color-theme-2 .dropdown-menu .dropdown-caret .caret-inner { 28 | border-bottom: 9px solid #2d3143; 29 | } 30 | .color-theme-2 .dropdown-menu .buttons { 31 | border-color: #272a3a; 32 | } 33 | .color-theme-2 .dropdown-menu .button { 34 | color: #62677f; 35 | } 36 | .color-theme-2 .dropdown-menu .button:hover { 37 | color: #f4f4f5; 38 | } 39 | .book .book-header .font-settings .font-enlarge { 40 | line-height: 30px; 41 | font-size: 1.4em; 42 | } 43 | .book .book-header .font-settings .font-reduce { 44 | line-height: 30px; 45 | font-size: 1em; 46 | } 47 | .book.color-theme-1 .book-body { 48 | color: #704214; 49 | background: #f3eacb; 50 | } 51 | .book.color-theme-1 .book-body .page-wrapper .page-inner section { 52 | background: #f3eacb; 53 | } 54 | .book.color-theme-2 .book-body { 55 | color: #bdcadb; 56 | background: #1c1f2b; 57 | } 58 | .book.color-theme-2 .book-body .page-wrapper .page-inner section { 59 | background: #1c1f2b; 60 | } 61 | .book.font-size-0 .book-body .page-inner section { 62 | font-size: 1.2rem; 63 | } 64 | .book.font-size-1 .book-body .page-inner section { 65 | font-size: 1.4rem; 66 | } 67 | .book.font-size-2 .book-body .page-inner section { 68 | font-size: 1.6rem; 69 | } 70 | .book.font-size-3 .book-body .page-inner section { 71 | font-size: 2.2rem; 72 | } 73 | .book.font-size-4 .book-body .page-inner section { 74 | font-size: 4rem; 75 | } 76 | .book.font-family-0 { 77 | font-family: Georgia, serif; 78 | } 79 | .book.font-family-1 { 80 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 81 | } 82 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal { 83 | color: #704214; 84 | } 85 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal a { 86 | color: inherit; 87 | } 88 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h1, 89 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h2, 90 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h3, 91 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h4, 92 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h5, 93 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h6 { 94 | color: inherit; 95 | } 96 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h1, 97 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h2 { 98 | border-color: inherit; 99 | } 100 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal h6 { 101 | color: inherit; 102 | } 103 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal hr { 104 | background-color: inherit; 105 | } 106 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal blockquote { 107 | border-color: inherit; 108 | } 109 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal pre, 110 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal code { 111 | background: #fdf6e3; 112 | color: #657b83; 113 | border-color: #f8df9c; 114 | } 115 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal .highlight { 116 | background-color: inherit; 117 | } 118 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal table th, 119 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal table td { 120 | border-color: #f5d06c; 121 | } 122 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal table tr { 123 | color: inherit; 124 | background-color: #fdf6e3; 125 | border-color: #444444; 126 | } 127 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal table tr:nth-child(2n) { 128 | background-color: #fbeecb; 129 | } 130 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal { 131 | color: #bdcadb; 132 | } 133 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal a { 134 | color: #3eb1d0; 135 | } 136 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h1, 137 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h2, 138 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h3, 139 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h4, 140 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h5, 141 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h6 { 142 | color: #fffffa; 143 | } 144 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h1, 145 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h2 { 146 | border-color: #373b4e; 147 | } 148 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal h6 { 149 | color: #373b4e; 150 | } 151 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal hr { 152 | background-color: #373b4e; 153 | } 154 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal blockquote { 155 | border-color: #373b4e; 156 | } 157 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal pre, 158 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal code { 159 | color: #9dbed8; 160 | background: #2d3143; 161 | border-color: #2d3143; 162 | } 163 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal .highlight { 164 | background-color: #282a39; 165 | } 166 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal table th, 167 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal table td { 168 | border-color: #3b3f54; 169 | } 170 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal table tr { 171 | color: #b6c2d2; 172 | background-color: #2d3143; 173 | border-color: #3b3f54; 174 | } 175 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal table tr:nth-child(2n) { 176 | background-color: #35394b; 177 | } 178 | .book.color-theme-1 .book-header { 179 | color: #afa790; 180 | background: transparent; 181 | } 182 | .book.color-theme-1 .book-header .btn { 183 | color: #afa790; 184 | } 185 | .book.color-theme-1 .book-header .btn:hover { 186 | color: #73553c; 187 | background: none; 188 | } 189 | .book.color-theme-1 .book-header h1 { 190 | color: #704214; 191 | } 192 | .book.color-theme-2 .book-header { 193 | color: #7e888b; 194 | background: transparent; 195 | } 196 | .book.color-theme-2 .book-header .btn { 197 | color: #3b3f54; 198 | } 199 | .book.color-theme-2 .book-header .btn:hover { 200 | color: #fffff5; 201 | background: none; 202 | } 203 | .book.color-theme-2 .book-header h1 { 204 | color: #bdcadb; 205 | } 206 | .book.color-theme-1 .book-body .navigation { 207 | color: #afa790; 208 | } 209 | .book.color-theme-1 .book-body .navigation:hover { 210 | color: #73553c; 211 | } 212 | .book.color-theme-2 .book-body .navigation { 213 | color: #383f52; 214 | } 215 | .book.color-theme-2 .book-body .navigation:hover { 216 | color: #fffff5; 217 | } 218 | /* 219 | * Theme 1 220 | */ 221 | .book.color-theme-1 .book-summary { 222 | color: #afa790; 223 | background: #111111; 224 | border-right: 1px solid rgba(0, 0, 0, 0.07); 225 | } 226 | .book.color-theme-1 .book-summary .book-search { 227 | background: transparent; 228 | } 229 | .book.color-theme-1 .book-summary .book-search input, 230 | .book.color-theme-1 .book-summary .book-search input:focus { 231 | border: 1px solid transparent; 232 | } 233 | .book.color-theme-1 .book-summary ul.summary li.divider { 234 | background: #7e888b; 235 | box-shadow: none; 236 | } 237 | .book.color-theme-1 .book-summary ul.summary li i.fa-check { 238 | color: #33cc33; 239 | } 240 | .book.color-theme-1 .book-summary ul.summary li.done > a { 241 | color: #877f6a; 242 | } 243 | .book.color-theme-1 .book-summary ul.summary li a, 244 | .book.color-theme-1 .book-summary ul.summary li span { 245 | color: #877f6a; 246 | background: transparent; 247 | font-weight: normal; 248 | } 249 | .book.color-theme-1 .book-summary ul.summary li.active > a, 250 | .book.color-theme-1 .book-summary ul.summary li a:hover { 251 | color: #704214; 252 | background: transparent; 253 | font-weight: normal; 254 | } 255 | /* 256 | * Theme 2 257 | */ 258 | .book.color-theme-2 .book-summary { 259 | color: #bcc1d2; 260 | background: #2d3143; 261 | border-right: none; 262 | } 263 | .book.color-theme-2 .book-summary .book-search { 264 | background: transparent; 265 | } 266 | .book.color-theme-2 .book-summary .book-search input, 267 | .book.color-theme-2 .book-summary .book-search input:focus { 268 | border: 1px solid transparent; 269 | } 270 | .book.color-theme-2 .book-summary ul.summary li.divider { 271 | background: #272a3a; 272 | box-shadow: none; 273 | } 274 | .book.color-theme-2 .book-summary ul.summary li i.fa-check { 275 | color: #33cc33; 276 | } 277 | .book.color-theme-2 .book-summary ul.summary li.done > a { 278 | color: #62687f; 279 | } 280 | .book.color-theme-2 .book-summary ul.summary li a, 281 | .book.color-theme-2 .book-summary ul.summary li span { 282 | color: #c1c6d7; 283 | background: transparent; 284 | font-weight: 600; 285 | } 286 | .book.color-theme-2 .book-summary ul.summary li.active > a, 287 | .book.color-theme-2 .book-summary ul.summary li a:hover { 288 | color: #f4f4f5; 289 | background: #252737; 290 | font-weight: 600; 291 | } 292 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-github-buttons/plugin.js: -------------------------------------------------------------------------------- 1 | // LICENSE : MIT 2 | "use strict"; 3 | require(['gitbook'], function (gitbook) { 4 | function addBeforeHeader(element) { 5 | jQuery('.book-header > h1').before(element); 6 | } 7 | 8 | function createButton(_ref) { 9 | var user = _ref.user; 10 | var repo = _ref.repo; 11 | var type = _ref.type; 12 | var size = _ref.size; 13 | var width = _ref.width; 14 | var height = _ref.height; 15 | var count = _ref.count; 16 | 17 | var extraParam = type === "watch" ? "&v=2" : ""; 18 | return '\n \n '; 19 | } 20 | 21 | function createUserButton(_ref2) { 22 | var user = _ref2.user; 23 | var size = _ref2.size; 24 | var width = _ref2.width; 25 | var height = _ref2.height; 26 | var count = _ref2.count; 27 | 28 | return '\n \n '; 29 | } 30 | 31 | function insertGitHubLink(button) { 32 | var user = button.user; 33 | var repo = button.repo; 34 | var type = button.type; 35 | var size = button.size; 36 | var width = button.width; 37 | var height = button.height; 38 | var count = button.count; 39 | 40 | var size = size || "large"; 41 | var width = width || (size === "large" ? "150" : "100"); 42 | var height = height || (size === "large" ? "30" : "20"); 43 | var count = typeof count === "boolean" ? count : false; 44 | 45 | if (type === 'follow') { 46 | var elementString = createUserButton({ 47 | user: user, 48 | size: size, 49 | width: width, 50 | height: height, 51 | count: count 52 | }); 53 | } else { 54 | var elementString = createButton({ 55 | user: user, 56 | repo: repo, 57 | type: type, 58 | size: size, 59 | width: width, 60 | height: height, 61 | count: count 62 | }); 63 | } 64 | addBeforeHeader(elementString); 65 | } 66 | 67 | function init(config) { 68 | config.buttons.forEach(insertGitHubLink); 69 | } 70 | 71 | // injected by html hook 72 | function getPluginConfig() { 73 | return window["gitbook-plugin-github-buttons"]; 74 | } 75 | 76 | // make sure configuration gets injected 77 | gitbook.events.bind('start', function (e, config) { 78 | window["gitbook-plugin-github-buttons"] = config["github-buttons"]; 79 | }); 80 | 81 | gitbook.events.bind('page.change', function () { 82 | init(getPluginConfig()); 83 | }); 84 | }); 85 | //# sourceMappingURL=plugin.js.map -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-github-buttons/plugin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/plugin.js"],"names":[],"mappings":";AACA,YAAY,CAAC;AACb,OAAO,CAAC,CAAC,SAAS,CAAC,EAAE,UAAU,OAAO,EAAE;AACpC,aAAS,eAAe,CAAC,OAAO,EAAE;AAC9B,cAAM,CAAC,mBAAmB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;KAC9C;;AAED,aAAS,YAAY,CAAC,IAQjB,EAAE;YAPH,IAAI,GADc,IAQjB,CAPD,IAAI;YACJ,IAAI,GAFc,IAQjB,CAND,IAAI;YACJ,IAAI,GAHc,IAQjB,CALD,IAAI;YACJ,IAAI,GAJc,IAQjB,CAJD,IAAI;YACJ,KAAK,GALa,IAQjB,CAHD,KAAK;YACL,MAAM,GANY,IAQjB,CAFD,MAAM;YACN,KAAK,GAPa,IAQjB,CADD,KAAK;;AAEL,YAAI,UAAU,GAAG,IAAI,KAAK,OAAO,GAAG,MAAM,GAAG,EAAE,CAAC;AAChD,yOAGuD,IAAI,cAAS,IAAI,cAAS,IAAI,eAAU,KAAK,cAAS,IAAI,GAAG,UAAU,kGAG7G,KAAK,qCACJ,MAAM,+CAElB;KACT;;AAED,aAAS,gBAAgB,CAAC,KAMrB,EAAE;YALH,IAAI,GADkB,KAMrB,CALD,IAAI;YACJ,IAAI,GAFkB,KAMrB,CAJD,IAAI;YACJ,KAAK,GAHiB,KAMrB,CAHD,KAAK;YACL,MAAM,GAJgB,KAMrB,CAFD,MAAM;YACN,KAAK,GALiB,KAMrB,CADD,KAAK;;AAEL,yOAGuD,IAAI,2BAAsB,KAAK,cAAS,IAAI,kGAGlF,KAAK,qCACJ,MAAM,+CAElB;KACT;;AAED,aAAS,gBAAgB,CAAC,MAAM,EAAE;YAE1B,IAAI,GAOJ,MAAM,CAPN,IAAI;YACJ,IAAI,GAMJ,MAAM,CANN,IAAI;YACJ,IAAI,GAKJ,MAAM,CALN,IAAI;YACJ,IAAI,GAIJ,MAAM,CAJN,IAAI;YACJ,KAAK,GAGL,MAAM,CAHN,KAAK;YACL,MAAM,GAEN,MAAM,CAFN,MAAM;YACN,KAAK,GACL,MAAM,CADN,KAAK;;AAGT,YAAI,IAAI,GAAG,IAAI,IAAI,OAAO,CAAC;AAC3B,YAAI,KAAK,GAAG,KAAK,KAAK,IAAI,KAAK,OAAO,GAAG,KAAK,GAAG,KAAK,CAAA,AAAC,CAAC;AACxD,YAAI,MAAM,GAAG,MAAM,KAAK,IAAI,KAAK,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA,AAAC,CAAC;AACxD,YAAI,KAAK,GAAG,OAAO,KAAK,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;;AAEvD,YAAI,IAAI,KAAK,QAAQ,EAAE;AACnB,gBAAI,aAAa,GAAG,gBAAgB,CAAC;AACjC,oBAAI,EAAJ,IAAI;AACJ,oBAAI,EAAJ,IAAI;AACJ,qBAAK,EAAL,KAAK;AACL,sBAAM,EAAN,MAAM;AACN,qBAAK,EAAL,KAAK;aACR,CAAC,CAAC;SACN,MAAM;AACH,gBAAI,aAAa,GAAG,YAAY,CAAC;AAC7B,oBAAI,EAAJ,IAAI;AACJ,oBAAI,EAAJ,IAAI;AACJ,oBAAI,EAAJ,IAAI;AACJ,oBAAI,EAAJ,IAAI;AACJ,qBAAK,EAAL,KAAK;AACL,sBAAM,EAAN,MAAM;AACN,qBAAK,EAAL,KAAK;aACR,CAAC,CAAC;SACN;AACD,uBAAe,CAAC,aAAa,CAAC,CAAC;KAClC;;AAED,aAAS,IAAI,CAAC,MAAM,EAAE;AAClB,cAAM,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;KAC5C;;;AAGD,aAAS,eAAe,GAAG;AACvB,eAAO,MAAM,CAAC,+BAA+B,CAAC,CAAC;KAClD;;;AAGD,WAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE;AAC9C,cAAM,CAAC,+BAA+B,CAAC,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;KACtE,CAAC,CAAC;;AAEH,WAAO,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY;AAC3C,YAAI,CAAC,eAAe,EAAE,CAAC,CAAC;KAC3B,CAAC,CAAC;CACN,CAAC,CAAC","file":"plugin.js","sourcesContent":["// LICENSE : MIT\n\"use strict\";\nrequire(['gitbook'], function (gitbook) {\n function addBeforeHeader(element) {\n jQuery('.book-header > h1').before(element)\n }\n\n function createButton({\n user,\n repo,\n type,\n size,\n width,\n height,\n count\n }) {\n var extraParam = type === \"watch\" ? \"&v=2\" : \"\";\n return `\n \n `;\n }\n\n function createUserButton({\n user,\n size,\n width,\n height,\n count\n }) {\n return `\n \n `;\n }\n\n function insertGitHubLink(button) {\n var {\n user,\n repo,\n type,\n size,\n width,\n height,\n count\n } = button;\n\n var size = size || \"large\";\n var width = width || (size === \"large\" ? \"150\" : \"100\");\n var height = height || (size === \"large\" ? \"30\" : \"20\");\n var count = typeof count === \"boolean\" ? count : false;\n\n if (type === 'follow') {\n var elementString = createUserButton({\n user,\n size,\n width,\n height,\n count \n });\n } else {\n var elementString = createButton({\n user,\n repo,\n type,\n size,\n width,\n height,\n count\n });\n }\n addBeforeHeader(elementString);\n }\n\n function init(config) {\n config.buttons.forEach(insertGitHubLink);\n }\n\n // injected by html hook\n function getPluginConfig() {\n return window[\"gitbook-plugin-github-buttons\"];\n }\n\n // make sure configuration gets injected\n gitbook.events.bind('start', function (e, config) {\n window[\"gitbook-plugin-github-buttons\"] = config[\"github-buttons\"];\n });\n\n gitbook.events.bind('page.change', function () {\n init(getPluginConfig());\n });\n});\n"]} -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-github/plugin.js: -------------------------------------------------------------------------------- 1 | require([ 'gitbook' ], function (gitbook) { 2 | gitbook.events.bind('start', function (e, config) { 3 | var githubURL = config.github.url; 4 | 5 | gitbook.toolbar.createButton({ 6 | icon: 'fa fa-github', 7 | label: 'GitHub', 8 | position: 'right', 9 | onClick: function() { 10 | window.open(githubURL) 11 | } 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-highlight/ebook.css: -------------------------------------------------------------------------------- 1 | pre, 2 | code { 3 | /* http://jmblog.github.io/color-themes-for-highlightjs */ 4 | /* Tomorrow Comment */ 5 | /* Tomorrow Red */ 6 | /* Tomorrow Orange */ 7 | /* Tomorrow Yellow */ 8 | /* Tomorrow Green */ 9 | /* Tomorrow Aqua */ 10 | /* Tomorrow Blue */ 11 | /* Tomorrow Purple */ 12 | } 13 | pre .hljs-comment, 14 | code .hljs-comment, 15 | pre .hljs-title, 16 | code .hljs-title { 17 | color: #8e908c; 18 | } 19 | pre .hljs-variable, 20 | code .hljs-variable, 21 | pre .hljs-attribute, 22 | code .hljs-attribute, 23 | pre .hljs-tag, 24 | code .hljs-tag, 25 | pre .hljs-regexp, 26 | code .hljs-regexp, 27 | pre .hljs-deletion, 28 | code .hljs-deletion, 29 | pre .ruby .hljs-constant, 30 | code .ruby .hljs-constant, 31 | pre .xml .hljs-tag .hljs-title, 32 | code .xml .hljs-tag .hljs-title, 33 | pre .xml .hljs-pi, 34 | code .xml .hljs-pi, 35 | pre .xml .hljs-doctype, 36 | code .xml .hljs-doctype, 37 | pre .html .hljs-doctype, 38 | code .html .hljs-doctype, 39 | pre .css .hljs-id, 40 | code .css .hljs-id, 41 | pre .css .hljs-class, 42 | code .css .hljs-class, 43 | pre .css .hljs-pseudo, 44 | code .css .hljs-pseudo { 45 | color: #c82829; 46 | } 47 | pre .hljs-number, 48 | code .hljs-number, 49 | pre .hljs-preprocessor, 50 | code .hljs-preprocessor, 51 | pre .hljs-pragma, 52 | code .hljs-pragma, 53 | pre .hljs-built_in, 54 | code .hljs-built_in, 55 | pre .hljs-literal, 56 | code .hljs-literal, 57 | pre .hljs-params, 58 | code .hljs-params, 59 | pre .hljs-constant, 60 | code .hljs-constant { 61 | color: #f5871f; 62 | } 63 | pre .ruby .hljs-class .hljs-title, 64 | code .ruby .hljs-class .hljs-title, 65 | pre .css .hljs-rules .hljs-attribute, 66 | code .css .hljs-rules .hljs-attribute { 67 | color: #eab700; 68 | } 69 | pre .hljs-string, 70 | code .hljs-string, 71 | pre .hljs-value, 72 | code .hljs-value, 73 | pre .hljs-inheritance, 74 | code .hljs-inheritance, 75 | pre .hljs-header, 76 | code .hljs-header, 77 | pre .hljs-addition, 78 | code .hljs-addition, 79 | pre .ruby .hljs-symbol, 80 | code .ruby .hljs-symbol, 81 | pre .xml .hljs-cdata, 82 | code .xml .hljs-cdata { 83 | color: #718c00; 84 | } 85 | pre .css .hljs-hexcolor, 86 | code .css .hljs-hexcolor { 87 | color: #3e999f; 88 | } 89 | pre .hljs-function, 90 | code .hljs-function, 91 | pre .python .hljs-decorator, 92 | code .python .hljs-decorator, 93 | pre .python .hljs-title, 94 | code .python .hljs-title, 95 | pre .ruby .hljs-function .hljs-title, 96 | code .ruby .hljs-function .hljs-title, 97 | pre .ruby .hljs-title .hljs-keyword, 98 | code .ruby .hljs-title .hljs-keyword, 99 | pre .perl .hljs-sub, 100 | code .perl .hljs-sub, 101 | pre .javascript .hljs-title, 102 | code .javascript .hljs-title, 103 | pre .coffeescript .hljs-title, 104 | code .coffeescript .hljs-title { 105 | color: #4271ae; 106 | } 107 | pre .hljs-keyword, 108 | code .hljs-keyword, 109 | pre .javascript .hljs-function, 110 | code .javascript .hljs-function { 111 | color: #8959a8; 112 | } 113 | pre .hljs, 114 | code .hljs { 115 | display: block; 116 | background: white; 117 | color: #4d4d4c; 118 | padding: 0.5em; 119 | } 120 | pre .coffeescript .javascript, 121 | code .coffeescript .javascript, 122 | pre .javascript .xml, 123 | code .javascript .xml, 124 | pre .tex .hljs-formula, 125 | code .tex .hljs-formula, 126 | pre .xml .javascript, 127 | code .xml .javascript, 128 | pre .xml .vbscript, 129 | code .xml .vbscript, 130 | pre .xml .css, 131 | code .xml .css, 132 | pre .xml .hljs-cdata, 133 | code .xml .hljs-cdata { 134 | opacity: 0.5; 135 | } 136 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-lunr/search-lunr.js: -------------------------------------------------------------------------------- 1 | require([ 2 | 'gitbook', 3 | 'jquery' 4 | ], function(gitbook, $) { 5 | // Define global search engine 6 | function LunrSearchEngine() { 7 | this.index = null; 8 | this.store = {}; 9 | this.name = 'LunrSearchEngine'; 10 | } 11 | 12 | // Initialize lunr by fetching the search index 13 | LunrSearchEngine.prototype.init = function() { 14 | var that = this; 15 | var d = $.Deferred(); 16 | 17 | $.getJSON(gitbook.state.basePath+'/search_index.json') 18 | .then(function(data) { 19 | // eslint-disable-next-line no-undef 20 | that.index = lunr.Index.load(data.index); 21 | that.store = data.store; 22 | d.resolve(); 23 | }); 24 | 25 | return d.promise(); 26 | }; 27 | 28 | // Search for a term and return results 29 | LunrSearchEngine.prototype.search = function(q, offset, length) { 30 | var that = this; 31 | var results = []; 32 | 33 | if (this.index) { 34 | results = $.map(this.index.search(q), function(result) { 35 | var doc = that.store[result.ref]; 36 | 37 | return { 38 | title: doc.title, 39 | url: doc.url, 40 | body: doc.summary || doc.body 41 | }; 42 | }); 43 | } 44 | 45 | return $.Deferred().resolve({ 46 | query: q, 47 | results: results.slice(0, length), 48 | count: results.length 49 | }).promise(); 50 | }; 51 | 52 | // Set gitbook research 53 | gitbook.events.bind('start', function(e, config) { 54 | var engine = gitbook.search.getEngine(); 55 | if (!engine) { 56 | gitbook.search.setEngine(LunrSearchEngine, config); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-search/search-engine.js: -------------------------------------------------------------------------------- 1 | require([ 2 | 'gitbook', 3 | 'jquery' 4 | ], function(gitbook, $) { 5 | // Global search objects 6 | var engine = null; 7 | var initialized = false; 8 | 9 | // Set a new search engine 10 | function setEngine(Engine, config) { 11 | initialized = false; 12 | engine = new Engine(config); 13 | 14 | init(config); 15 | } 16 | 17 | // Initialize search engine with config 18 | function init(config) { 19 | if (!engine) throw new Error('No engine set for research. Set an engine using gitbook.research.setEngine(Engine).'); 20 | 21 | return engine.init(config) 22 | .then(function() { 23 | initialized = true; 24 | gitbook.events.trigger('search.ready'); 25 | }); 26 | } 27 | 28 | // Launch search for query q 29 | function query(q, offset, length) { 30 | if (!initialized) throw new Error('Search has not been initialized'); 31 | return engine.search(q, offset, length); 32 | } 33 | 34 | // Get stats about search 35 | function getEngine() { 36 | return engine? engine.name : null; 37 | } 38 | 39 | function isInitialized() { 40 | return initialized; 41 | } 42 | 43 | // Initialize gitbook.search 44 | gitbook.search = { 45 | setEngine: setEngine, 46 | getEngine: getEngine, 47 | query: query, 48 | isInitialized: isInitialized 49 | }; 50 | }); -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-search/search.css: -------------------------------------------------------------------------------- 1 | /* 2 | This CSS only styled the search results section, not the search input 3 | It defines the basic interraction to hide content when displaying results, etc 4 | */ 5 | #book-search-results .search-results { 6 | display: none; 7 | } 8 | #book-search-results .search-results ul.search-results-list { 9 | list-style-type: none; 10 | padding-left: 0; 11 | } 12 | #book-search-results .search-results ul.search-results-list li { 13 | margin-bottom: 1.5rem; 14 | padding-bottom: 0.5rem; 15 | /* Highlight results */ 16 | } 17 | #book-search-results .search-results ul.search-results-list li p em { 18 | background-color: rgba(255, 220, 0, 0.4); 19 | font-style: normal; 20 | } 21 | #book-search-results .search-results .no-results { 22 | display: none; 23 | } 24 | #book-search-results.open .search-results { 25 | display: block; 26 | } 27 | #book-search-results.open .search-noresults { 28 | display: none; 29 | } 30 | #book-search-results.no-results .search-results .has-results { 31 | display: none; 32 | } 33 | #book-search-results.no-results .search-results .no-results { 34 | display: block; 35 | } 36 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-search/search.js: -------------------------------------------------------------------------------- 1 | require([ 2 | 'gitbook', 3 | 'jquery' 4 | ], function(gitbook, $) { 5 | var MAX_RESULTS = 15; 6 | var MAX_DESCRIPTION_SIZE = 500; 7 | 8 | var usePushState = (typeof history.pushState !== 'undefined'); 9 | 10 | // DOM Elements 11 | var $body = $('body'); 12 | var $bookSearchResults; 13 | var $searchInput; 14 | var $searchList; 15 | var $searchTitle; 16 | var $searchResultsCount; 17 | var $searchQuery; 18 | 19 | // Throttle search 20 | function throttle(fn, wait) { 21 | var timeout; 22 | 23 | return function() { 24 | var ctx = this, args = arguments; 25 | if (!timeout) { 26 | timeout = setTimeout(function() { 27 | timeout = null; 28 | fn.apply(ctx, args); 29 | }, wait); 30 | } 31 | }; 32 | } 33 | 34 | function displayResults(res) { 35 | $bookSearchResults.addClass('open'); 36 | 37 | var noResults = res.count == 0; 38 | $bookSearchResults.toggleClass('no-results', noResults); 39 | 40 | // Clear old results 41 | $searchList.empty(); 42 | 43 | // Display title for research 44 | $searchResultsCount.text(res.count); 45 | $searchQuery.text(res.query); 46 | 47 | // Create an
      • element for each result 48 | res.results.forEach(function(res) { 49 | var $li = $('
      • ', { 50 | 'class': 'search-results-item' 51 | }); 52 | 53 | var $title = $('

        '); 54 | 55 | var $link = $('', { 56 | 'href': gitbook.state.basePath + '/' + res.url, 57 | 'text': res.title 58 | }); 59 | 60 | var content = res.body.trim(); 61 | if (content.length > MAX_DESCRIPTION_SIZE) { 62 | content = content.slice(0, MAX_DESCRIPTION_SIZE).trim()+'...'; 63 | } 64 | var $content = $('

        ').html(content); 65 | 66 | $link.appendTo($title); 67 | $title.appendTo($li); 68 | $content.appendTo($li); 69 | $li.appendTo($searchList); 70 | }); 71 | } 72 | 73 | function launchSearch(q) { 74 | // Add class for loading 75 | $body.addClass('with-search'); 76 | $body.addClass('search-loading'); 77 | 78 | // Launch search query 79 | throttle(gitbook.search.query(q, 0, MAX_RESULTS) 80 | .then(function(results) { 81 | displayResults(results); 82 | }) 83 | .always(function() { 84 | $body.removeClass('search-loading'); 85 | }), 1000); 86 | } 87 | 88 | function closeSearch() { 89 | $body.removeClass('with-search'); 90 | $bookSearchResults.removeClass('open'); 91 | } 92 | 93 | function launchSearchFromQueryString() { 94 | var q = getParameterByName('q'); 95 | if (q && q.length > 0) { 96 | // Update search input 97 | $searchInput.val(q); 98 | 99 | // Launch search 100 | launchSearch(q); 101 | } 102 | } 103 | 104 | function bindSearch() { 105 | // Bind DOM 106 | $searchInput = $('#book-search-input input'); 107 | $bookSearchResults = $('#book-search-results'); 108 | $searchList = $bookSearchResults.find('.search-results-list'); 109 | $searchTitle = $bookSearchResults.find('.search-results-title'); 110 | $searchResultsCount = $searchTitle.find('.search-results-count'); 111 | $searchQuery = $searchTitle.find('.search-query'); 112 | 113 | // Launch query based on input content 114 | function handleUpdate() { 115 | var q = $searchInput.val(); 116 | 117 | if (q.length == 0) { 118 | closeSearch(); 119 | } 120 | else { 121 | launchSearch(q); 122 | } 123 | } 124 | 125 | // Detect true content change in search input 126 | // Workaround for IE < 9 127 | var propertyChangeUnbound = false; 128 | $searchInput.on('propertychange', function(e) { 129 | if (e.originalEvent.propertyName == 'value') { 130 | handleUpdate(); 131 | } 132 | }); 133 | 134 | // HTML5 (IE9 & others) 135 | $searchInput.on('input', function(e) { 136 | // Unbind propertychange event for IE9+ 137 | if (!propertyChangeUnbound) { 138 | $(this).unbind('propertychange'); 139 | propertyChangeUnbound = true; 140 | } 141 | 142 | handleUpdate(); 143 | }); 144 | 145 | // Push to history on blur 146 | $searchInput.on('blur', function(e) { 147 | // Update history state 148 | if (usePushState) { 149 | var uri = updateQueryString('q', $(this).val()); 150 | history.pushState({ path: uri }, null, uri); 151 | } 152 | }); 153 | } 154 | 155 | gitbook.events.on('page.change', function() { 156 | bindSearch(); 157 | closeSearch(); 158 | 159 | // Launch search based on query parameter 160 | if (gitbook.search.isInitialized()) { 161 | launchSearchFromQueryString(); 162 | } 163 | }); 164 | 165 | gitbook.events.on('search.ready', function() { 166 | bindSearch(); 167 | 168 | // Launch search from query param at start 169 | launchSearchFromQueryString(); 170 | }); 171 | 172 | function getParameterByName(name) { 173 | var url = window.location.href; 174 | name = name.replace(/[\[\]]/g, '\\$&'); 175 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)', 'i'), 176 | results = regex.exec(url); 177 | if (!results) return null; 178 | if (!results[2]) return ''; 179 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 180 | } 181 | 182 | function updateQueryString(key, value) { 183 | value = encodeURIComponent(value); 184 | 185 | var url = window.location.href; 186 | var re = new RegExp('([?&])' + key + '=.*?(&|#|$)(.*)', 'gi'), 187 | hash; 188 | 189 | if (re.test(url)) { 190 | if (typeof value !== 'undefined' && value !== null) 191 | return url.replace(re, '$1' + key + '=' + value + '$2$3'); 192 | else { 193 | hash = url.split('#'); 194 | url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, ''); 195 | if (typeof hash[1] !== 'undefined' && hash[1] !== null) 196 | url += '#' + hash[1]; 197 | return url; 198 | } 199 | } 200 | else { 201 | if (typeof value !== 'undefined' && value !== null) { 202 | var separator = url.indexOf('?') !== -1 ? '&' : '?'; 203 | hash = url.split('#'); 204 | url = hash[0] + separator + key + '=' + value; 205 | if (typeof hash[1] !== 'undefined' && hash[1] !== null) 206 | url += '#' + hash[1]; 207 | return url; 208 | } 209 | else 210 | return url; 211 | } 212 | } 213 | }); 214 | -------------------------------------------------------------------------------- /docs/gitbook/gitbook-plugin-sharing-plus/buttons.js: -------------------------------------------------------------------------------- 1 | require(['gitbook', 'jquery'], function(gitbook, $) { 2 | function site(label, icon, link) { 3 | return { 4 | label: label, 5 | icon: 'fa fa-' + icon, 6 | onClick: function (e) { 7 | e.preventDefault(); 8 | window.open(link); 9 | } 10 | }; 11 | } 12 | 13 | var url = encodeURIComponent(location.href); 14 | var title = encodeURIComponent(document.title); 15 | 16 | var SITES = { 17 | douban: site('豆瓣', 'share', 'http://shuo.douban.com/!service/share?href=' + url + '&name=' + title), 18 | facebook: site('Facebook', 'facebook', 'http://www.facebook.com/sharer/sharer.php?s=100&p[url]=' + url), 19 | google: site('Google+', 'google-plus', 'https://plus.google.com/share?url=' + url), 20 | hatenaBookmark: site('はてなブックマーク', 'bold', 'http://b.hatena.ne.jp/entry/' + url), 21 | instapaper: site('instapaper', 'instapaper', 'http://www.instapaper.com/text?u=' + url), 22 | line: site('LINE', 'comment', 'http://line.me/R/msg/text/?' + title + ' ' + url), 23 | linkedin: site('Linkedin', 'linkedin', 'https://www.linkedin.com/shareArticle?mini=true&url=' + url), 24 | messenger: site('Facebook Messenger', 'commenting', 'fb-messenger://share?link=' + url), 25 | pocket: site('Pocket', 'get-pocket', 'https://getpocket.com/save?url=' + url + '&title=' + title), 26 | qq: site('QQ', 'qq', 'http://connect.qq.com/widget/shareqq/index.html?url=' + url + '&title=' + title), 27 | qzone: site('QQ空间', 'star', 'http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=' + url + '&title=' + title), 28 | stumbleupon: site('StumbleUpon', 'stumbleupon', 'http://www.stumbleupon.com/submit?url=' + url + '&title=' + title), 29 | twitter: site('Twitter', 'twitter', 'https://twitter.com/intent/tweet?url=' + title + '&text=' + title), 30 | viber: site('Viber', 'volume-control-phone', 'viber://forward?text='+ url + ' ' + title), 31 | vk: site('VK', 'vk', 'http://vkontakte.ru/share.php?url=' + url), 32 | weibo: site('新浪微博', 'weibo', 'http://service.weibo.com/share/share.php?content=utf-8&url=' + url + '&title=' + title), 33 | whatsapp: site('WhatsApp', 'whatsapp', 'whatsapp://send?text='+ url + ' ' + title), 34 | }; 35 | 36 | gitbook.events.bind('start', function(e, config) { 37 | var opts = config.sharing; 38 | 39 | // Create dropdown menu 40 | var menu = $.map(opts.all, function(id) { 41 | var site = SITES[id]; 42 | 43 | return { 44 | text: site.label, 45 | onClick: site.onClick 46 | }; 47 | }); 48 | 49 | // Create main button with dropdown 50 | if (menu.length > 0) { 51 | gitbook.toolbar.createButton({ 52 | icon: 'fa fa-share-alt', 53 | label: 'Share', 54 | position: 'right', 55 | dropdown: [menu] 56 | }); 57 | } 58 | 59 | // Direct actions to share 60 | $.each(SITES, function(sideId, site) { 61 | if (!opts[sideId]) return; 62 | 63 | gitbook.toolbar.createButton({ 64 | icon: site.icon, 65 | label: site.text, 66 | position: 'right', 67 | onClick: site.onClick 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /docs/gitbook/images/apple-touch-icon-precomposed-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/images/apple-touch-icon-precomposed-152.png -------------------------------------------------------------------------------- /docs/gitbook/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/gitbook/images/favicon.ico -------------------------------------------------------------------------------- /docs/source/images/GUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/source/images/GUI.png -------------------------------------------------------------------------------- /docs/source/images/M3U8URL2File.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/source/images/M3U8URL2File.gif -------------------------------------------------------------------------------- /docs/source/images/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/source/images/alipay.png -------------------------------------------------------------------------------- /docs/source/images/muxSetJson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/source/images/muxSetJson.png -------------------------------------------------------------------------------- /docs/source/images/直接使用.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilaoda/N_m3u8DL-CLI/985f6e57c33de552561edd8f7b141a69bd75484c/docs/source/images/直接使用.gif --------------------------------------------------------------------------------