├── .gitignore ├── LICENSE ├── README.md └── ResourceMonitor ├── .gitignore ├── .idea ├── .gitignore └── .idea.ResourceMonitor │ ├── .idea │ ├── .gitignore │ ├── contentModel.xml │ ├── encodings.xml │ ├── indexLayout.xml │ ├── modules.xml │ ├── projectSettingsUpdater.xml │ └── vcs.xml │ └── riderModule.iml ├── ResourceMonitor.Test ├── Aria2DownloaderTests.cs ├── ResourceMonitor.Test.csproj ├── RulesContainerTests.cs ├── TorrentServiceTests.cs ├── TransmissionDownloaderTests.cs └── valid.torrent ├── ResourceMonitor.sln └── ResourceMonitor ├── .dockerignore ├── Controllers └── ServerController.cs ├── Dockerfile ├── Models ├── DandanplayApi │ ├── AutoDownloadRule.cs │ ├── AutoDownloadRuleListResponse.cs │ ├── AutoDownloadRuleSyncRequest.cs │ ├── LoginRequest.cs │ ├── LoginResponse.cs │ ├── ResponseBase.cs │ └── UserPrivileges.cs ├── Downloader │ └── DownloaderTask.cs ├── ResApi │ ├── ResourceInfo.cs │ ├── ResourceList.cs │ ├── SubgroupInfo.cs │ ├── SubgroupList.cs │ ├── TypeInfo.cs │ └── TypeList.cs ├── WeatherForecast.cs └── Web │ └── ServerStatusViewModel.cs ├── Program.cs ├── Properties └── launchSettings.json ├── ResourceMonitor.csproj ├── Services ├── Declaration │ ├── IDandanplayApi.cs │ ├── IDownloader.cs │ ├── IMagnetApi.cs │ ├── IResApi.cs │ ├── IRulesContainer.cs │ └── ITorrentService.cs └── Implementation │ ├── Aria2 │ ├── AriaFile.cs │ ├── AriaGlobalStatus.cs │ ├── AriaManager.cs │ ├── AriaOption.cs │ ├── AriaServer.cs │ ├── AriaSession.cs │ ├── AriaStatus.cs │ ├── AriaTorrent.cs │ ├── AriaUri.cs │ └── AriaVersionInfo.cs │ ├── Aria2Downloader.cs │ ├── CheckNewResourcesBackgroundService.cs │ ├── RulesContainer.cs │ ├── SyncRulesBackgroundService.cs │ ├── TorrentService.cs │ └── TransmissionDownloader.cs ├── Startup.cs ├── appsettings.json └── wwwroot ├── index.html └── scripts ├── jquery-3.5.1.min.js └── vue.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 弹弹play资源监视器 dandanplay-resmonitor 2 | 3 | ![License](https://img.shields.io/github/license/kaedei/dandanplay-resmonitor) 4 | ![GitHub repo size](https://img.shields.io/github/repo-size/kaedei/dandanplay-resmonitor) 5 | ![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/kaedei/dandanplay-resmonitor) 6 | 7 | [![Build status](https://dev.azure.com/kaedei/dandanplay-resmonitor/_apis/build/status/dandanplay-resmonitor-ASP.NET%20Core-CI)](https://dev.azure.com/kaedei/dandanplay-resmonitor/_build/latest?definitionId=11) 8 | [![DockerHub](https://img.shields.io/docker/v/kaedei/dandanplay-resmonitor?label=docker%20hub&style=plastic)](https://hub.docker.com/r/kaedei/dandanplay-resmonitor/tags) 9 | [![Github Release](https://vsrm.dev.azure.com/kaedei/_apis/public/Release/badge/9739e25e-bed4-42b9-9872-fa328f18783b/2/3)](https://github.com/kaedei/dandanplay-resmonitor/releases) 10 | 11 | ## 一、介绍 12 | 13 | ### 一句话简介 14 | 15 | 这是一个动漫资源自动监视+下载的小工具。 16 | 17 | ### 它能做什么 18 | 19 | 此工具模拟了 [弹弹play播放器](http://www.dandanplay.com/) Windows版上的“自动下载”功能,能够根据设定好的监视规则自动检测网上是否有新的资源发布,当发现新的资源后自动通知下载工具新建任务并开始下载。 20 | 21 | 简单来说,一般从网上找资源的流程是 `访问资源站->搜索->选择->下载->观看`。此工具可以帮你把 `访问资源站->搜索->选择` 这前几步变成完全自动的,不用等每次资源有更新时再手动操作一遍了,能为你节省很多时间和精力。 22 | 23 | 此工具没有内置下载器,但是支持调用已有的 __Aria2__、__Transmission__ 等下载工具进行BT下载。 24 | 25 | ### 开发工具 26 | 27 | 此项目使用 `ASP.NET Core` 技术编写,编程语言使用了 `C#`,在 macOS 10.15 下的 `Jetbrains Rider` IDE 进行开发,在 Windows 下使用 Visual Studio 2022 进行开发。SDK版本目前为 `.NET SDK 6.0.400`。 28 | 29 | ### 付费 30 | 31 | 为了补贴各种服务器费用和日常开销,本工具虽是开源产品,但并不是免费使用的。试用到期后,您需要赞助作者才可以持续使用。新用户将会在您第一次使用弹弹play登录账号并同步“自动下载规则”后向您赠送7天的免费试用时间。 32 | 33 | 34 | ## 二、怎样使用 35 | 36 | ### 在弹弹play客户端上注册用户并创建规则 37 | 38 | 1. 去弹弹play官网 [dandanplay.com](http://www.dandanplay.com) 下载安装最新版的Windows客户端或UWP客户端 39 | 40 | 2. 在首页或者“个人中心”页登录或注册一个新的弹弹play账号 41 | 42 | 3. 登录弹弹play账号后,点击上方菜单导航到“资源”-“自动下载”页面 43 | 44 | 4. 新建一个“自动下载”规则,填写对应的监视参数。新建完毕后此规则会被自动同步到云端。 45 | 46 | ### 在本机运行 47 | 48 | 1. 在[Github发布页面](https://github.com/kaedei/dandanplay-resmonitor/releases)下载编译好的最新版本,将所有文件解压到本地的某个文件夹中。目前我们已编译好面向多个操作系统的版本,您可以选择: 49 | 50 | - `xxxxxxxx_dotnet.zip` 这是通用的版本,可以运行在大部分环境中,但是需要您先安装[.NET Core 3.1 运行时或SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1) 51 | 52 | - `xxxxxxxx_win-x64.zip` 这是面向64位Windows编译的版本 53 | 54 | - `xxxxxxxx_linux-x64.zip` 这是面向64位Linux系统编译的版本 55 | 56 | - `xxxxxxxx_osx-x64.zip` 这是面向macOS 10.12或以上系统编译的版本 57 | 58 | 2. 进入解压后的文件夹,使用文本编辑器软件打开 `appsettings.json` 文件,修改软件配置。这里将会有一篇详细的说明文档:[本机运行](https://github.com/kaedei/dandanplay-resmonitor/wiki/%E4%BF%AE%E6%94%B9%E9%85%8D%E7%BD%AE#%E6%9C%AC%E6%9C%BA%E8%BF%90%E8%A1%8C)。建议修改完成后备份此配置文件,免得将来升级时被不小心覆盖。 59 | 60 | 3. 之后即可运行此程序了: 61 | 62 | - 对于通用版(dotnet.zip),使用命令行(cmd、Terminal等)进入此文件夹,然后执行 `dotnet ResourceMonitor.dll` 命令,即可启动。 63 | 64 | - 对于Windows版(win-x64.zip),直接双击启动 ResourceMonitor.exe 文件 65 | 66 | - 对于Linux版(linux-x64.zip)和macOS版(osx-x64.zip),直接双击启动 ResourceMonitor 文件。 67 | 68 | 程序启动后将会持续运行,直到你关闭窗口或是按 Ctrl+C 结束进程。 69 | 70 | ### docker镜像部署 71 | 72 | [![DockerHub](https://images.microbadger.com/badges/version/kaedei/dandanplay-resmonitor.svg)](https://hub.docker.com/r/kaedei/dandanplay-resmonitor/tags) 73 | 74 | 1. 运行命令 `docker run -it kaedei/dandanplay-resmonitor` 即可启动此工具 75 | 76 | 2. 添加 `-e` 参数可以(通过改变环境变量的方式)自定义程序运行参数。这一步是必需的,详情请参考文档 [docker部署](https://github.com/kaedei/dandanplay-resmonitor/wiki/%E4%BF%AE%E6%94%B9%E9%85%8D%E7%BD%AE#docker) 77 | 78 | 3. 添加 `-d` 参数可以让容器在后台持续运行 79 | 80 | 4. 添加 `-p 本机端口:80` 可以暴露内部的web服务,之后通过浏览器访问刚才指定的本机IP和端口(例如 http://192.168.1.100:8080) 即可看到此工具的当前状态。 81 | 82 | 5. 此工具的日志文件位于容器内的 `/app/log` 目录,可以通过 `-v` 挂载本机目录,然后查看这些日志文件。 83 | 84 | ## 三、开发者相关 85 | 86 | ### 配置开发环境 87 | 88 | 1. 通过 [GitHub的打包下载](https://github.com/kaedei/dandanplay-resmonitor/archive/master.zip) 或通过 `git clone` 下载最新版的代码到本机目录 89 | 90 | 2. 安装 `.NET Core SDK 3.1.100` 或更高的版本。[官方下载链接](https://dotnet.microsoft.com/download/dotnet-core/3.1) 91 | 92 | 3. 使用 Visual Studio 2019、Rider 等开发工具打开 `/ResourceMonitor/ResourceMonitor.sln` 文件,即可打开解决方案。 93 | 94 | ### 编译代码 95 | 96 | 1. 命令行导航到源代码所在目录,例如 `C:\code\dandanplay-resmonitor` 97 | 98 | 2. 进入`ResourceMonitor.csproj` 所在的文件夹,大概在 `C:\code\dandanplay-resmonitor\ResourceMonitor\ResourceMonitor\ResourceMonitor.csproj` 99 | 100 | 3. 运行命令 `dotnet build -c Release` 即可以Release模式编译代码,编译后的文件位于 `\bin\Release\netcoreapp3.1\publish` 文件夹中 101 | 102 | ### 其他说明 103 | 104 | - 为了保证程序可以随时启动、终止并保证状态一致,代码中没有设计存储层,即不会将状态保存到外部数据库、Redis、文件系统中。 105 | 106 | - 可以通过扩展实现 `IDownloader` 接口来支持更多的下载工具 107 | -------------------------------------------------------------------------------- /ResourceMonitor/.gitignore: -------------------------------------------------------------------------------- 1 | appsettings.Development.json 2 | log -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedei/dandanplay-resmonitor/2650b3e8421bcbf92e499ec362873b1bc2c9bfb5/ResourceMonitor/.idea/.gitignore -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/contentModel.xml: -------------------------------------------------------------------------------- 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 | 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 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 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 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/projectSettingsUpdater.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ResourceMonitor/.idea/.idea.ResourceMonitor/riderModule.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.Test/Aria2DownloaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using Moq; 9 | using ResourceMonitor.Services.Implementation; 10 | 11 | namespace ResourceMonitor.Test 12 | { 13 | [TestClass] 14 | public class Aria2DownloaderTests 15 | { 16 | IConfigurationRoot configuration = new ConfigurationBuilder() 17 | .AddInMemoryCollection(new Dictionary 18 | { 19 | {"Aria2:Url", "http://localhost:6800/jsonrpc"}, 20 | {"Aria2:Token", "token"} 21 | }) 22 | .Build(); 23 | 24 | [TestMethod] 25 | public async Task TryConnect() 26 | { 27 | var mockFactory = new Mock(); 28 | mockFactory.Setup(c => c.CreateClient()).Returns(new HttpClient()); 29 | var downloader = new Aria2Downloader(configuration, mockFactory.Object , new NullLogger()); 30 | var result = await downloader.TryConnect(); 31 | Assert.IsTrue(result); 32 | } 33 | 34 | [TestMethod] 35 | public async Task AddNewTorrentTask() 36 | { 37 | var mockFactory = new Mock(); 38 | mockFactory.Setup(c => c.CreateClient()).Returns(new HttpClient()); 39 | var downloader = new Aria2Downloader(configuration, mockFactory.Object, new NullLogger()); 40 | var torrentBytes = File.ReadAllBytes("valid.torrent"); 41 | await downloader.AddNewTorrentTask(torrentBytes); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.Test/ResourceMonitor.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Always 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.Test/RulesContainerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace ResourceMonitor.Test 4 | { 5 | [TestClass] 6 | public class RulesContainerTests 7 | { 8 | [TestMethod] 9 | public void TestMethod1() 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.Test/TorrentServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Refit; 7 | using ResourceMonitor.Services.Declaration; 8 | using ResourceMonitor.Services.Implementation; 9 | 10 | namespace ResourceMonitor.Test 11 | { 12 | [TestClass] 13 | public class TorrentServiceTests 14 | { 15 | [TestMethod] 16 | public void IsTorrentFileValid_Pass() 17 | { 18 | var service = new TorrentService(null, new NullLogger()); 19 | var torrentBytes = File.ReadAllBytes("valid.torrent"); 20 | Assert.IsTrue(service.IsTorrentFileValid(torrentBytes)); 21 | } 22 | 23 | [TestMethod] 24 | public void IsTorrentFileValid_NotPass() 25 | { 26 | var service = new TorrentService(null, new NullLogger()); 27 | var torrentBytes = File.ReadAllBytes("valid.torrent"); 28 | //修改一些文件内容 29 | for (int i = 100; i < 200; i++) 30 | { 31 | torrentBytes[i] = 0; 32 | } 33 | Assert.IsFalse(service.IsTorrentFileValid(torrentBytes)); 34 | } 35 | 36 | [TestMethod] 37 | public void NormalizeMagnetUrl_Pass_40() 38 | { 39 | var service = new TorrentService(null, new NullLogger()); 40 | var magnet = "magnet:?xt=urn:btih:08c2c30ca85cf78ed147488a431d9aee50824a7b"; 41 | var after = service.NormalizeMagnetUrl(magnet); 42 | Assert.AreEqual(magnet, after); 43 | } 44 | 45 | [TestMethod] 46 | public void NormalizeMagnetUrl_Pass_32() 47 | { 48 | var service = new TorrentService(null, new NullLogger()); 49 | var magnet = "magnet:?xt=urn:btih:BDBMGDFILT3Y5UKHJCFEGHM25ZIIEST3"; 50 | var after = service.NormalizeMagnetUrl(magnet); 51 | Assert.AreEqual("magnet:?xt=urn:btih:08c2c30ca85cf78ed147488a431d9aee50824a7b", after); 52 | } 53 | 54 | [TestMethod] 55 | public void NormalizeMagnetUrl_Pass_Tracker() 56 | { 57 | var service = new TorrentService(null, new NullLogger()); 58 | var magnet = 59 | "magnet:?xt=urn:btih:4cda49aa1c28db946e89ecb6e18482c8d347b41d&tr=udp://9.rarbg.to:2710/announce&tr=udp://9.rarbg.me:2710/announce&tr=http://tr.cili001.com:8070/announce&tr=http://tracker.trackerfix.com:80/announce&tr=udp://open.demonii.com:1337&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://p4p.arenabg.com:1337&tr=wss://tracker.openwebtorrent.com&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.fastcast.nz"; 60 | var after = service.NormalizeMagnetUrl(magnet); 61 | Assert.AreEqual("magnet:?xt=urn:btih:4cda49aa1c28db946e89ecb6e18482c8d347b41d", after); 62 | } 63 | 64 | [TestMethod] 65 | public void GetHash_Pass_32() 66 | { 67 | var service = new TorrentService(null, new NullLogger()); 68 | var magnet = "magnet:?xt=urn:btih:BDBMGDFILT3Y5UKHJCFEGHM25ZIIEST3"; 69 | var hash = service.GetHash(magnet); 70 | Assert.AreEqual("08c2c30ca85cf78ed147488a431d9aee50824a7b", hash); 71 | } 72 | 73 | [TestMethod] 74 | public void GetHash_Pass_40() 75 | { 76 | var service = new TorrentService(null, new NullLogger()); 77 | var magnet = "magnet:?xt=urn:btih:08c2c30ca85cf78ed147488a431d9aee50824a7b"; 78 | var hash = service.GetHash(magnet); 79 | Assert.AreEqual("08c2c30ca85cf78ed147488a431d9aee50824a7b", hash); 80 | } 81 | 82 | [TestMethod] 83 | public void GetHash_NotPass_UpperCase() 84 | { 85 | var service = new TorrentService(null, new NullLogger()); 86 | var magnet = "magnet:?xt=urn:btih:08c2c30ca85cf78ed147488a431d9aee50824a7b"; 87 | var hash = service.GetHash(magnet); 88 | Assert.AreNotEqual("08C2C30CA85CF78ED147488A431D9AEE50824A7B", hash); 89 | } 90 | 91 | //[TestMethod] 92 | public async Task DownloadTorrent_Pass_Online() 93 | { 94 | var api = RestService.For("https://m2t.chinacloudsites.cn"); 95 | var service = new TorrentService(api, new NullLogger()); 96 | var magnet = "magnet:?xt=urn:btih:08c2c30ca85cf78ed147488a431d9aee50824a7b"; 97 | var torrentBytes = await service.DownloadTorrent(magnet); 98 | Assert.IsTrue(service.IsTorrentFileValid(torrentBytes)); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.Test/TransmissionDownloaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Logging.Abstractions; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using ResourceMonitor.Services.Implementation; 10 | 11 | namespace ResourceMonitor.Test 12 | { 13 | [TestClass] 14 | public class TransmissionDownloaderTests 15 | { 16 | IConfigurationRoot configuration = new ConfigurationBuilder() 17 | .AddInMemoryCollection(new Dictionary 18 | { 19 | {"Transmission:Url", "http://localhost:9091/transmission/rpc"}, 20 | {"Transmission:Login", "admin"}, 21 | {"Transmission:Password", "admin"} 22 | }) 23 | .Build(); 24 | 25 | [TestMethod] 26 | public async Task TryConnect() 27 | { 28 | var downloader = new TransmissionDownloader(configuration, new NullLogger()); 29 | Assert.IsTrue(await downloader.TryConnect()); 30 | } 31 | 32 | [TestMethod] 33 | public async Task GetAllTasks() 34 | { 35 | var downloader = new TransmissionDownloader(configuration, new NullLogger()); 36 | var tasks = await downloader.GetAllTasks(); 37 | foreach (var task in tasks) 38 | { 39 | Debug.WriteLine(task.ToString()); 40 | } 41 | } 42 | 43 | [TestMethod] 44 | public async Task AddNewTorrentTask() 45 | { 46 | var downloader = new TransmissionDownloader(configuration, new NullLogger()); 47 | var torrentBytes = File.ReadAllBytes("valid.torrent"); 48 | await downloader.AddNewTorrentTask(torrentBytes); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.Test/valid.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaedei/dandanplay-resmonitor/2650b3e8421bcbf92e499ec362873b1bc2c9bfb5/ResourceMonitor/ResourceMonitor.Test/valid.torrent -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceMonitor", "ResourceMonitor\ResourceMonitor.csproj", "{6803E3E6-61BA-4DB0-BD0C-1A810D493E9C}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceMonitor.Test", "ResourceMonitor.Test\ResourceMonitor.Test.csproj", "{6C8A0394-AA65-4E45-8E11-16067A94EE99}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {6803E3E6-61BA-4DB0-BD0C-1A810D493E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {6803E3E6-61BA-4DB0-BD0C-1A810D493E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {6803E3E6-61BA-4DB0-BD0C-1A810D493E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {6803E3E6-61BA-4DB0-BD0C-1A810D493E9C}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {6C8A0394-AA65-4E45-8E11-16067A94EE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {6C8A0394-AA65-4E45-8E11-16067A94EE99}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {6C8A0394-AA65-4E45-8E11-16067A94EE99}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {6C8A0394-AA65-4E45-8E11-16067A94EE99}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/.dockerignore: -------------------------------------------------------------------------------- 1 | appsettings.*.json -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Controllers/ServerController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Logging; 7 | using ResourceMonitor.Models.Web; 8 | using ResourceMonitor.Services.Declaration; 9 | 10 | namespace ResourceMonitor.Controllers 11 | { 12 | [ApiController] 13 | [Route("server")] 14 | public class ServerController: ControllerBase 15 | { 16 | private readonly IRulesContainer _rulesContainer; 17 | private readonly IConfiguration _configuration; 18 | private readonly ILogger _logger; 19 | 20 | public ServerController(IRulesContainer rulesContainer, IConfiguration configuration, ILogger logger) 21 | { 22 | _rulesContainer = rulesContainer; 23 | _configuration = configuration; 24 | _logger = logger; 25 | } 26 | 27 | [Route("status")] 28 | public ServerStatusViewModel GetServerStatus() 29 | { 30 | _logger.LogInformation("(Web)刷新服务器当前状态"); 31 | var model = new ServerStatusViewModel 32 | { 33 | version = Assembly.GetExecutingAssembly().GetName().Version.ToString(), 34 | downloader = _configuration["Downloader"], 35 | rules = _rulesContainer.LocalRules.ToArray() 36 | }; 37 | return model; 38 | } 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime 2 | WORKDIR /app 3 | COPY . /app 4 | EXPOSE 80 5 | ENTRYPOINT ["dotnet", "ResourceMonitor.dll"] -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/AutoDownloadRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ResourceMonitor.Models.DandanplayApi 4 | { 5 | /* 6 | * { 7 | "id": "string", 8 | "description": "string", 9 | "keyword": "string", 10 | "subgroupId": 0, 11 | "typeId": 0, 12 | "maxCount": 0, 13 | "autoStart": true, 14 | "createdTime": "2020-06-21T09:30:22.124Z", 15 | "startTime": "2020-06-21T09:30:22.124Z", 16 | "subgroupName": "string", 17 | "typeName": "string", 18 | "chooseNewerIfDuplicate": true, 19 | "limitFileSize": 0, 20 | "version": 0, 21 | "isShared": true 22 | } 23 | */ 24 | 25 | /// 26 | /// 下载规则详情 27 | /// 28 | public class AutoDownloadRule 29 | { 30 | public string id { get; set; } 31 | public string description { get; set; } 32 | public string keyword { get; set; } 33 | public int? subgroupId { get; set; } 34 | public int? typeId { get; set; } 35 | public int maxCount { get; set; } 36 | public bool autoStart { get; set; } 37 | public DateTime createdTime { get; set; } 38 | public DateTime? startTime { get; set; } 39 | public string subgroupName { get; set; } 40 | public string typeName { get; set; } 41 | public bool chooseNewerIfDuplicate { get; set; } 42 | public int limitFileSize { get; set; } 43 | public long version { get; set; } 44 | 45 | public override string ToString() 46 | { 47 | return $"{nameof(id)}: {id}, {nameof(description)}: {description}, {nameof(keyword)}: {keyword}, {nameof(subgroupId)}: {subgroupId}, {nameof(typeId)}: {typeId}, {nameof(maxCount)}: {maxCount}, {nameof(autoStart)}: {autoStart}, {nameof(createdTime)}: {createdTime}, {nameof(startTime)}: {startTime}, {nameof(subgroupName)}: {subgroupName}, {nameof(typeName)}: {typeName}, {nameof(chooseNewerIfDuplicate)}: {chooseNewerIfDuplicate}, {nameof(limitFileSize)}: {limitFileSize}, {nameof(version)}: {version}"; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/AutoDownloadRuleListResponse.cs: -------------------------------------------------------------------------------- 1 | namespace ResourceMonitor.Models.DandanplayApi 2 | { 3 | public class AutoDownloadRuleListResponse : ResponseBase 4 | { 5 | public AutoDownloadRule[] rules { get; set; } 6 | public string[] removedRuleIds { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/AutoDownloadRuleSyncRequest.cs: -------------------------------------------------------------------------------- 1 | namespace ResourceMonitor.Models.DandanplayApi 2 | { 3 | public class AutoDownloadRuleSyncRequest 4 | { 5 | public string[] currentRuleIds { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | namespace ResourceMonitor.Models.DandanplayApi 2 | { 3 | public class LoginRequest 4 | { 5 | public string userName { get; set; } 6 | public string password { get; set; } 7 | public string appId { get; set; } 8 | public long unixTimestamp { get; set; } 9 | public string hash { get; set; } 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/LoginResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ResourceMonitor.Models.DandanplayApi 4 | { 5 | /* 6 | { 7 | "registerRequired": true, 8 | "userId": 0, 9 | "userName": "string", 10 | "legacyTokenNumber": 0, 11 | "token": "string", 12 | "tokenExpireTime": "2020-06-21T09:30:21.927Z", 13 | "userType": "string", 14 | "screenName": "string", 15 | "profileImage": "string", 16 | "appScope": "string", 17 | "errorCode": 0, 18 | "success": true, 19 | "errorMessage": "string" 20 | } 21 | * 22 | * 23 | */ 24 | public class LoginResponse : ResponseBase 25 | { 26 | public bool registerRequired { get; set; } 27 | public int userId { get; set; } 28 | public string userName { get; set; } 29 | public string token { get; set; } 30 | public DateTime tokenExpireTime { get; set; } 31 | public string userType { get; set; } 32 | public string screenName { get; set; } 33 | public string appScope { get; set; } 34 | 35 | public UserPrivileges privileges { get; set; } 36 | } 37 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/ResponseBase.cs: -------------------------------------------------------------------------------- 1 | namespace ResourceMonitor.Models.DandanplayApi 2 | { 3 | public class ResponseBase 4 | { 5 | public int errorCode { get; set; } 6 | public bool success { get; set; } 7 | public string errorMessage { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/DandanplayApi/UserPrivileges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ResourceMonitor.Models.DandanplayApi 4 | { 5 | public class UserPrivileges 6 | { 7 | public DateTime? resmonitor { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/Downloader/DownloaderTask.cs: -------------------------------------------------------------------------------- 1 | namespace ResourceMonitor.Models.Downloader 2 | { 3 | public class DownloaderTask 4 | { 5 | public string Id { get; set; } 6 | public string Magnet { get; set; } 7 | public string Name { get; set; } 8 | 9 | public override string ToString() 10 | { 11 | return $"{nameof(Id)}: {Id}, {nameof(Magnet)}: {Magnet}, {nameof(Name)}: {Name}"; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/ResApi/ResourceInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.RegularExpressions; 4 | using System.Xml.Serialization; 5 | 6 | namespace ResourceMonitor.Models.ResApi 7 | { 8 | /// 9 | /// 资源详细信息 10 | /// 11 | [XmlRoot("Resource", Namespace = "")] 12 | public class ResourceInfo 13 | { 14 | /// 15 | /// 资源标题 16 | /// 17 | public string Title { get; set; } 18 | 19 | /// 20 | /// 类型ID 21 | /// 22 | public int TypeId { get; set; } 23 | 24 | /// 25 | /// 类型名称 26 | /// 27 | public string TypeName { get; set; } 28 | 29 | /// 30 | /// 字幕组ID 31 | /// 32 | public int SubgroupId { get; set; } 33 | 34 | /// 35 | /// 字幕组名称 36 | /// 37 | public string SubgroupName { get; set; } 38 | 39 | /// 40 | /// 磁力链接 41 | /// 42 | public string Magnet { get; set; } 43 | 44 | /// 45 | /// 发布页 46 | /// 47 | public string PageUrl { get; set; } 48 | 49 | /// 50 | /// 文件大小 51 | /// 52 | public string FileSize { get; set; } 53 | 54 | /// 55 | /// 发布时间 56 | /// 57 | public string PublishDate { get; set; } 58 | 59 | /// 60 | /// 发布时间(DateTime类型) 61 | /// 62 | public DateTime PublishDateTime => DateTime.Parse(PublishDate, CultureInfo.InvariantCulture); 63 | 64 | /// 65 | /// 文件大小(MB) 66 | /// 67 | public decimal FileSizeInMB 68 | { 69 | get 70 | { 71 | var r = Regex.Match(FileSize, @"^(?\d+(?:\.\d{1,3})?)(?MB|GB)$", RegexOptions.IgnoreCase); 72 | if (!r.Success) 73 | { 74 | return 0; 75 | } 76 | var size = decimal.Parse(r.Groups["size"].Value, CultureInfo.InvariantCulture); 77 | var unit = r.Groups["unit"].Value.Equals("MB", StringComparison.InvariantCultureIgnoreCase) ? 1 : 1024; 78 | return size * unit; 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/ResApi/ResourceList.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace ResourceMonitor.Models.ResApi 4 | { 5 | /// 6 | /// 资源列表 7 | /// 8 | [XmlRoot("Resources")] 9 | public class ResourceList 10 | { 11 | [XmlAttribute("HasMore")] 12 | public bool HasMore { get; set; } 13 | /// 14 | /// 包含每个资源详细信息的集合 15 | /// 16 | [XmlElement("Resource")] 17 | public ResourceInfo[] Resources { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/ResApi/SubgroupInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Xml.Serialization; 3 | 4 | namespace ResourceMonitor.Models.ResApi 5 | { 6 | /// 7 | /// 字幕组详细信息 8 | /// 9 | public class SubgroupInfo 10 | { 11 | /// 12 | /// 字幕组ID 13 | /// 14 | [XmlAttribute("Id")] 15 | public int Id { get; set; } 16 | /// 17 | /// 字幕组名称 18 | /// 19 | [XmlAttribute("Name")] 20 | public string Name { get; set; } 21 | 22 | #region IdEqualityComparer 23 | 24 | private sealed class IdEqualityComparer : IEqualityComparer 25 | { 26 | public bool Equals(SubgroupInfo x, SubgroupInfo y) 27 | { 28 | if (ReferenceEquals(x, y)) return true; 29 | if (ReferenceEquals(x, null)) return false; 30 | if (ReferenceEquals(y, null)) return false; 31 | if (x.GetType() != y.GetType()) return false; 32 | return x.Id == y.Id; 33 | } 34 | 35 | public int GetHashCode(SubgroupInfo obj) 36 | { 37 | return obj.Id; 38 | } 39 | } 40 | 41 | private static readonly IEqualityComparer m_idComparerInstance = new IdEqualityComparer(); 42 | 43 | public static IEqualityComparer IdComparer 44 | { 45 | get { return m_idComparerInstance; } 46 | } 47 | 48 | #endregion 49 | } 50 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/ResApi/SubgroupList.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace ResourceMonitor.Models.ResApi 4 | { 5 | /// 6 | /// 字幕组列表 7 | /// 8 | [XmlRoot("Subgroups")] 9 | public class SubgroupList 10 | { 11 | /// 12 | /// 包含字幕组详细信息的集合 13 | /// 14 | [XmlElement("Subgroup")] 15 | public SubgroupInfo[] Subgroups { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/ResApi/TypeInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Xml.Serialization; 3 | 4 | namespace ResourceMonitor.Models.ResApi 5 | { 6 | /// 7 | /// 资源类型信息 8 | /// 9 | public class TypeInfo 10 | { 11 | /// 12 | /// 资源类型ID 13 | /// 14 | [XmlAttribute("Id")] 15 | public int Id { get; set; } 16 | 17 | /// 18 | /// 资源类型名称 19 | /// 20 | [XmlAttribute("Name")] 21 | public string Name { get; set; } 22 | 23 | #region IdEqualityComparer 24 | 25 | private sealed class IdEqualityComparer : IEqualityComparer 26 | { 27 | public bool Equals(TypeInfo x, TypeInfo y) 28 | { 29 | if (ReferenceEquals(x, y)) return true; 30 | if (ReferenceEquals(x, null)) return false; 31 | if (ReferenceEquals(y, null)) return false; 32 | if (x.GetType() != y.GetType()) return false; 33 | return x.Id == y.Id; 34 | } 35 | 36 | public int GetHashCode(TypeInfo obj) 37 | { 38 | return obj.Id; 39 | } 40 | } 41 | 42 | private static readonly IEqualityComparer m_idComparerInstance = new IdEqualityComparer(); 43 | 44 | public static IEqualityComparer IdComparer 45 | { 46 | get { return m_idComparerInstance; } 47 | } 48 | 49 | #endregion 50 | } 51 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/ResApi/TypeList.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace ResourceMonitor.Models.ResApi 4 | { 5 | /// 6 | /// 资源类型列表 7 | /// 8 | [XmlRoot("Types")] 9 | public class TypeList 10 | { 11 | /// 12 | /// 包含资源类型信息的集合 13 | /// 14 | [XmlElement("Type")] 15 | public TypeInfo[] Types { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ResourceMonitor.Models 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Models/Web/ServerStatusViewModel.cs: -------------------------------------------------------------------------------- 1 | using ResourceMonitor.Models.DandanplayApi; 2 | 3 | namespace ResourceMonitor.Models.Web 4 | { 5 | public class ServerStatusViewModel 6 | { 7 | public string version { get; set; } 8 | public string downloader { get; set; } 9 | public AutoDownloadRule[] rules { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | using ResourceMonitor.Services; 11 | using Serilog; 12 | using Serilog.Events; 13 | 14 | namespace ResourceMonitor 15 | { 16 | public class Program 17 | { 18 | public static int Main(string[] args) 19 | { 20 | Log.Logger = new LoggerConfiguration() 21 | .MinimumLevel.Debug() 22 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) 23 | .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) 24 | .Enrich.FromLogContext() 25 | .WriteTo.Console() 26 | .WriteTo.File(@"log/log-.txt", rollingInterval: RollingInterval.Day) 27 | .CreateLogger(); 28 | 29 | try 30 | { 31 | Log.Information("启动 ResMonitor..."); 32 | CreateHostBuilder(args).Build().Run(); 33 | return 0; 34 | } 35 | catch (Exception ex) 36 | { 37 | Log.Fatal(ex, "ResMonitor 因意外情况终止"); 38 | return 1; 39 | } 40 | finally 41 | { 42 | Log.CloseAndFlush(); 43 | } 44 | } 45 | 46 | public static IHostBuilder CreateHostBuilder(string[] args) => 47 | Host.CreateDefaultBuilder(args) 48 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) 49 | .UseSerilog(); 50 | } 51 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:29656", 8 | "sslPort": 44301 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "ResourceMonitor": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/ResourceMonitor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | kaedei 6 | Copyright kaedei 2020-2021 7 | https://github.com/kaedei/dandanplay-resmonitor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Always 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | 31 | true 32 | PreserveNewest 33 | PreserveNewest 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Declaration/IDandanplayApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Refit; 3 | using ResourceMonitor.Models.DandanplayApi; 4 | 5 | namespace ResourceMonitor.Services.Declaration 6 | { 7 | public interface IDandanplayApi 8 | { 9 | [Post("/api/v2/login")] 10 | Task Login([Body] LoginRequest request); 11 | 12 | [Get("/api/v2/login/renew")] 13 | Task Renew([Header("Authorization")] string authorization); 14 | 15 | [Post("/api/v2/sync/autodownload")] 16 | Task SyncAutoDownloadRules([Body] AutoDownloadRuleSyncRequest request, 17 | [Header("Authorization")] string authorization); 18 | } 19 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Declaration/IDownloader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using ResourceMonitor.Models.Downloader; 4 | 5 | namespace ResourceMonitor.Services.Declaration 6 | { 7 | public interface IDownloader 8 | { 9 | Task TryConnect(); 10 | 11 | Task> GetAllTasks(); 12 | 13 | Task IfTaskExists(string id, string url); 14 | 15 | Task AddNewTorrentTask(byte[] torrentBytes); 16 | } 17 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Declaration/IMagnetApi.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using Refit; 4 | 5 | namespace ResourceMonitor.Services.Declaration 6 | { 7 | public interface IMagnetApi 8 | { 9 | [Get("/Magnet/Parse")] 10 | Task ParseMagnet(string magnet); 11 | } 12 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Declaration/IResApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Refit; 3 | using ResourceMonitor.Models.ResApi; 4 | 5 | namespace ResourceMonitor.Services.Declaration 6 | { 7 | public interface IResApi 8 | { 9 | [Get("/list")] 10 | Task List(string keyword, int? subgroupId, int? typeId); 11 | } 12 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Declaration/IRulesContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using ResourceMonitor.Models.DandanplayApi; 3 | 4 | namespace ResourceMonitor.Services.Declaration 5 | { 6 | public interface IRulesContainer 7 | { 8 | bool IsUpdating { get; set; } 9 | ImmutableList LocalRules { get; } 10 | void SyncWithServerRules(AutoDownloadRuleListResponse serverRules); 11 | } 12 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Declaration/ITorrentService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ResourceMonitor.Services.Declaration 4 | { 5 | public interface ITorrentService 6 | { 7 | bool IsTorrentFileValid(byte[] torrentBytes); 8 | 9 | string NormalizeMagnetUrl(string magnet); 10 | 11 | string GetHash(string magnet); 12 | 13 | Task DownloadTorrent(string magnet); 14 | } 15 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace AriaNet.Attributes 6 | { 7 | public class AriaFile 8 | { 9 | [JsonProperty("index")] 10 | public string Index { get; set; } 11 | 12 | [JsonProperty("length")] 13 | public string Length { get; set; } 14 | 15 | [JsonProperty("completedLength")] 16 | public string CompletedLength { get; set; } 17 | 18 | [JsonProperty("path")] 19 | public string Path { get; set; } 20 | 21 | [JsonProperty("selected")] 22 | public string Selected { get; set; } 23 | 24 | //Not Implement this now. Ref: https://aria2.github.io/manual/en/html/aria2c.html#aria2.getUris 25 | //[JsonProperty("uris")] 26 | //public List Uris { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaGlobalStatus.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace AriaNet.Attributes 4 | { 5 | [JsonObject] 6 | public class AriaGlobalStatus 7 | { 8 | [JsonProperty("downloadSpeed")] 9 | public int DownloadSpeed { get; set; } 10 | 11 | [JsonProperty("numActive")] 12 | public int ActiveTaskCount { get; set; } 13 | 14 | [JsonProperty("numStopped")] 15 | public int StoppedTaskCount { get; set; } 16 | 17 | [JsonProperty("numWaiting")] 18 | public int WaitingTaskCount { get; set; } 19 | 20 | [JsonProperty("uploadSpeed")] 21 | public int UploadSpeed { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using AriaNet.Attributes; 11 | using Newtonsoft.Json; 12 | 13 | namespace AriaNet 14 | { 15 | public class AriaManager 16 | { 17 | private HttpClient m_client; 18 | 19 | private readonly string m_rpcUrl; 20 | 21 | private readonly string m_token; 22 | 23 | public AriaManager(string rpcUrl, string token, HttpClient client) 24 | { 25 | m_rpcUrl = rpcUrl; 26 | m_token = token; 27 | m_client = client; 28 | } 29 | 30 | private async Task RpcInvoke(string method, params object[] parameters) 31 | { 32 | var p = parameters?.ToList() ?? new List(1); 33 | if (!string.IsNullOrWhiteSpace(m_token)) 34 | { 35 | p.Insert(0, $"token:{m_token}"); 36 | } 37 | var content = new StringContent(JsonConvert.SerializeObject(new 38 | { 39 | jsonrpc = "2.0", 40 | id = "resmonitor", 41 | method = method, 42 | @params = p 43 | })); 44 | content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); 45 | var response = await m_client.PostAsync(m_rpcUrl, content); 46 | var responseString = await response.Content.ReadAsStringAsync(); 47 | var result = JsonConvert.DeserializeAnonymousType(responseString, new 48 | { 49 | result = default(T), 50 | error = new 51 | { 52 | code = 0, 53 | message = string.Empty 54 | } 55 | }); 56 | if (result.error == null || result.error.code == 0) 57 | { 58 | return result.result; 59 | } 60 | else 61 | { 62 | throw new Exception($"[{result.error.code}] {result.error.message}"); 63 | } 64 | } 65 | 66 | public async Task AddUri(List uriList) 67 | { 68 | return await RpcInvoke("aria2.addUri", uriList); 69 | } 70 | 71 | public async Task AddUri(List uriList, string userAgent, string referrer) 72 | { 73 | return await RpcInvoke("aria2.addUri", uriList, 74 | new Dictionary 75 | { 76 | {"user-agent", userAgent}, 77 | {"referer", referrer} 78 | }); 79 | } 80 | 81 | public async Task AddMetaLink(string filePath) 82 | { 83 | var metaLinkBase64 = Convert.ToBase64String(File.ReadAllBytes(filePath)); 84 | return await RpcInvoke("aria2.addMetalink", metaLinkBase64); 85 | } 86 | 87 | public async Task AddTorrent(string filePath) 88 | { 89 | return await AddTorrent(File.ReadAllBytes(filePath)); 90 | } 91 | 92 | public async Task AddTorrent(byte[] torrentBytes) 93 | { 94 | var torrentBase64 = Convert.ToBase64String(torrentBytes); 95 | return await RpcInvoke("aria2.addTorrent", torrentBase64); 96 | } 97 | 98 | public async Task RemoveTask(string gid, bool forceRemove = false) 99 | { 100 | if (!forceRemove) 101 | { 102 | return await RpcInvoke("aria2.remove", gid); 103 | } 104 | else 105 | { 106 | return await RpcInvoke("aria2.forceRemove", gid); 107 | } 108 | } 109 | 110 | public async Task PauseTask(string gid, bool forcePause = false) 111 | { 112 | if (!forcePause) 113 | { 114 | return await RpcInvoke("aria2.pause", gid); 115 | } 116 | else 117 | { 118 | return await RpcInvoke("aria2.forcePause", gid); 119 | } 120 | } 121 | 122 | public async Task PauseAllTasks() 123 | { 124 | return (await RpcInvoke("aria2.pauseAll")).Contains("OK"); 125 | } 126 | 127 | public async Task UnpauseAllTasks() 128 | { 129 | return (await RpcInvoke("aria2.unpauseAll")).Contains("OK"); 130 | } 131 | 132 | public async Task UnpauseTask(string gid) 133 | { 134 | return await RpcInvoke("aria2.unpause", gid); 135 | } 136 | 137 | public async Task GetStatus(string gid) 138 | { 139 | return await RpcInvoke("aria2.tellStatus", gid); 140 | } 141 | 142 | public async Task GetAllStatus() 143 | { 144 | var task1 = GetActiveStatus(); 145 | var task2 = GetWaitingStatus(); 146 | var task3 = GetStoppedStatus(); 147 | await Task.WhenAll(task1, task2, task3); 148 | return task1.Result.Concat(task2.Result).Concat(task3.Result).ToArray(); 149 | } 150 | 151 | public async Task GetUris(string gid) 152 | { 153 | return await RpcInvoke("aria2.getUris", gid); 154 | } 155 | 156 | public async Task GetFiles(string gid) 157 | { 158 | return await RpcInvoke("aria2.getFiles", gid); 159 | } 160 | 161 | public async Task GetPeers(string gid) 162 | { 163 | return await RpcInvoke("aria2.getPeers", gid); 164 | } 165 | 166 | public async Task GetServers(string gid) 167 | { 168 | return await RpcInvoke("aria2.getServers", gid); 169 | } 170 | 171 | public async Task GetActiveStatus(string gid) 172 | { 173 | return await RpcInvoke("aria2.tellActive", gid); 174 | } 175 | 176 | public async Task GetActiveStatus() 177 | { 178 | return await RpcInvoke("aria2.tellActive"); 179 | } 180 | 181 | public async Task GetWaitingStatus() 182 | { 183 | return await RpcInvoke("aria2.tellWaiting", 0, 999); 184 | } 185 | 186 | public async Task GetStoppedStatus() 187 | { 188 | return await RpcInvoke("aria2.tellStopped", 0, 999); 189 | } 190 | 191 | public async Task GetOption(string gid) 192 | { 193 | return await RpcInvoke("aria2.getOption", gid); 194 | } 195 | 196 | 197 | public async Task ChangeOption(string gid, AriaOption option) 198 | { 199 | return (await RpcInvoke("aria2.changeOption", gid, option)) 200 | .Contains("OK"); 201 | } 202 | 203 | public async Task GetGlobalOption() 204 | { 205 | return await RpcInvoke("aria2.getGlobalOption"); 206 | } 207 | 208 | public async Task ChangeGlobalOption(AriaOption option) 209 | { 210 | return (await RpcInvoke("aria2.changeGlobalOption", option)) 211 | .Contains("OK"); 212 | } 213 | 214 | public async Task GetGlobalStatus() 215 | { 216 | return await RpcInvoke("aria2.getGlobalStat"); 217 | } 218 | 219 | public async Task PurgeDownloadResult() 220 | { 221 | return (await RpcInvoke("aria2.purgeDownloadResult")).Contains("OK"); 222 | } 223 | 224 | public async Task RemoveDownloadResult(string gid) 225 | { 226 | return (await RpcInvoke("aria2.removeDownloadResult", gid)) 227 | .Contains("OK"); 228 | } 229 | 230 | public async Task GetVersion() 231 | { 232 | return await RpcInvoke("aria2.getVersion"); 233 | } 234 | 235 | public async Task GetSessionInfo() 236 | { 237 | return await RpcInvoke("aria2.getSessionInfo"); 238 | } 239 | 240 | public async Task Shutdown(bool forceShutdown = false) 241 | { 242 | if (!forceShutdown) 243 | { 244 | return (await RpcInvoke("aria2.shutdown")).Contains("OK"); 245 | } 246 | else 247 | { 248 | return (await RpcInvoke("aria2.forceShutdown")).Contains("OK"); 249 | } 250 | } 251 | 252 | public async Task SaveSession() 253 | { 254 | return (await RpcInvoke("aria2.saveSession")).Contains("OK"); 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaOption.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace AriaNet.Attributes 4 | { 5 | [JsonObject] 6 | public class AriaOption 7 | { 8 | [JsonProperty("all-proxy")] 9 | public string AllProxy { get; set; } 10 | 11 | [JsonProperty("all-proxy-passwd")] 12 | public string AllProxyPasswd { get; set; } 13 | 14 | [JsonProperty("all-proxy-user")] 15 | public string AllProxyUser { get; set; } 16 | 17 | [JsonProperty("allow-overwrite")] 18 | public string AllowOverwrite { get; set; } 19 | 20 | [JsonProperty("allow-piece-length-change")] 21 | public string AllowPieceLengthChange { get; set; } 22 | 23 | [JsonProperty("always-resume")] 24 | public string AlwaysResume { get; set; } 25 | 26 | [JsonProperty("async-dns")] 27 | public string AsyncDns { get; set; } 28 | 29 | [JsonProperty("auto-file-renaming")] 30 | public string AutoFileRenaming { get; set; } 31 | 32 | [JsonProperty("bt-enable-hook-after-hash-check")] 33 | public string BtEnableHookAfterHashCheck { get; set; } 34 | 35 | [JsonProperty("bt-enable-lpd")] 36 | public string BtEnableLpd { get; set; } 37 | 38 | [JsonProperty("bt-exclude-tracker")] 39 | public string BtExcludeTracker { get; set; } 40 | 41 | [JsonProperty("bt-external-ip")] 42 | public string BtExternalIp { get; set; } 43 | 44 | [JsonProperty("bt-force-encryption")] 45 | public string BtForceEncryption { get; set; } 46 | 47 | [JsonProperty("bt-hash-check-seed")] 48 | public string BtHashCheckSeed { get; set; } 49 | 50 | [JsonProperty("bt-max-peers")] 51 | public string BtMaxPeers { get; set; } 52 | 53 | [JsonProperty("bt-metadata-only")] 54 | public string BtMetadataOnly { get; set; } 55 | 56 | [JsonProperty("bt-min-crypto-level")] 57 | public string BtMinCryptoLevel { get; set; } 58 | 59 | [JsonProperty("bt-prioritize-piece")] 60 | public string BtPrioritizePiece { get; set; } 61 | 62 | [JsonProperty("bt-remove-unselected-file")] 63 | public string BtRemoveUnselectedFile { get; set; } 64 | 65 | [JsonProperty("bt-request-peer-speed-limit")] 66 | public string BtRequestPeerSpeedLimit { get; set; } 67 | 68 | [JsonProperty("bt-require-crypto")] 69 | public string BtRequireCrypto { get; set; } 70 | 71 | [JsonProperty("bt-save-metadata")] 72 | public string BtSaveMetadata { get; set; } 73 | 74 | [JsonProperty("bt-seed-unverified")] 75 | public string BtSeedUnverified { get; set; } 76 | 77 | [JsonProperty("bt-stop-timeout")] 78 | public string BtStopTimeout { get; set; } 79 | 80 | [JsonProperty("bt-tracker")] 81 | public string BtTracker { get; set; } 82 | 83 | [JsonProperty("bt-tracker-connect-timeout")] 84 | public string BtTrackerConnectTimeout { get; set; } 85 | 86 | [JsonProperty("bt-tracker-interval")] 87 | public string BtTrackerInterval { get; set; } 88 | 89 | [JsonProperty("bt-tracker-timeout")] 90 | public string BtTrackerTimeout { get; set; } 91 | 92 | [JsonProperty("check-integrity")] 93 | public string CheckIntegrity { get; set; } 94 | 95 | [JsonProperty("checksum")] 96 | public string Checksum { get; set; } 97 | 98 | [JsonProperty("conditional-get")] 99 | public string ConditionalGet { get; set; } 100 | 101 | [JsonProperty("connect-timeout")] 102 | public string ConnectTimeout { get; set; } 103 | 104 | [JsonProperty("content-disposition-default-utf8")] 105 | public string ContentDispositionDefaultUtf8 { get; set; } 106 | 107 | [JsonProperty("continue")] 108 | public string Continue { get; set; } 109 | 110 | [JsonProperty("dir")] 111 | public string Dir { get; set; } 112 | 113 | [JsonProperty("dry-run")] 114 | public string DryRun { get; set; } 115 | 116 | [JsonProperty("enable-http-keep-alive")] 117 | public string EnableHttpKeepAlive { get; set; } 118 | 119 | [JsonProperty("enable-http-pipelining")] 120 | public string EnableHttpPipelining { get; set; } 121 | 122 | [JsonProperty("enable-mmap")] 123 | public string EnableMmap { get; set; } 124 | 125 | [JsonProperty("enable-peer-exchange")] 126 | public string EnablePeerExchange { get; set; } 127 | 128 | [JsonProperty("file-allocation")] 129 | public string FileAllocation { get; set; } 130 | 131 | [JsonProperty("follow-metalink")] 132 | public string FollowMetalink { get; set; } 133 | 134 | [JsonProperty("follow-torrent")] 135 | public string FollowTorrent { get; set; } 136 | 137 | [JsonProperty("force-save")] 138 | public string ForceSave { get; set; } 139 | 140 | [JsonProperty("ftp-passwd")] 141 | public string FtpPasswd { get; set; } 142 | 143 | [JsonProperty("ftp-pasv")] 144 | public string FtpPasv { get; set; } 145 | 146 | [JsonProperty("ftp-proxy")] 147 | public string FtpProxy { get; set; } 148 | 149 | [JsonProperty("ftp-proxy-passwd")] 150 | public string FtpProxyPasswd { get; set; } 151 | 152 | [JsonProperty("ftp-proxy-user")] 153 | public string FtpProxyUser { get; set; } 154 | 155 | [JsonProperty("ftp-reuse-connection")] 156 | public string FtpReuseConnection { get; set; } 157 | 158 | [JsonProperty("ftp-type")] 159 | public string FtpType { get; set; } 160 | 161 | [JsonProperty("ftp-user")] 162 | public string FtpUser { get; set; } 163 | 164 | [JsonProperty("gid")] 165 | public string Gid { get; set; } 166 | 167 | [JsonProperty("hash-check-only")] 168 | public string HashCheckOnly { get; set; } 169 | 170 | [JsonProperty("header")] 171 | public string Header { get; set; } 172 | 173 | [JsonProperty("http-accept-gzip")] 174 | public string HttpAcceptGzip { get; set; } 175 | 176 | [JsonProperty("http-auth-challenge")] 177 | public string HttpAuthChallenge { get; set; } 178 | 179 | [JsonProperty("http-no-cache")] 180 | public string HttpNoCache { get; set; } 181 | 182 | [JsonProperty("http-passwd")] 183 | public string HttpPasswd { get; set; } 184 | 185 | [JsonProperty("http-proxy")] 186 | public string HttpProxy { get; set; } 187 | 188 | [JsonProperty("http-proxy-passwd")] 189 | public string HttpProxyPasswd { get; set; } 190 | 191 | [JsonProperty("http-proxy-user")] 192 | public string HttpProxyUser { get; set; } 193 | 194 | [JsonProperty("http-user")] 195 | public string HttpUser { get; set; } 196 | 197 | [JsonProperty("https-proxy")] 198 | public string HttpsProxy { get; set; } 199 | 200 | [JsonProperty("https-proxy-passwd")] 201 | public string HttpsProxyPasswd { get; set; } 202 | 203 | [JsonProperty("https-proxy-user")] 204 | public string HttpsProxyUser { get; set; } 205 | 206 | [JsonProperty("index-out")] 207 | public string IndexOut { get; set; } 208 | 209 | [JsonProperty("lowest-speed-limit")] 210 | public string LowestSpeedLimit { get; set; } 211 | 212 | [JsonProperty("max-connection-per-server")] 213 | public string MaxConnectionPerServer { get; set; } 214 | 215 | [JsonProperty("max-download-limit")] 216 | public string MaxDownloadLimit { get; set; } 217 | 218 | [JsonProperty("max-file-not-found")] 219 | public string MaxFileNotFound { get; set; } 220 | 221 | [JsonProperty("max-mmap-limit")] 222 | public string MaxMmapLimit { get; set; } 223 | 224 | [JsonProperty("max-resume-failure-tries")] 225 | public string MaxResumeFailureTries { get; set; } 226 | 227 | [JsonProperty("max-tries")] 228 | public string MaxTries { get; set; } 229 | 230 | [JsonProperty("max-upload-limit")] 231 | public string MaxUploadLimit { get; set; } 232 | 233 | [JsonProperty("metalink-base-uri")] 234 | public string MetalinkBaseUri { get; set; } 235 | 236 | [JsonProperty("metalink-enable-unique-protocol")] 237 | public string MetalinkEnableUniqueProtocol { get; set; } 238 | 239 | [JsonProperty("metalink-language")] 240 | public string MetalinkLanguage { get; set; } 241 | 242 | [JsonProperty("metalink-location")] 243 | public string MetalinkLocation { get; set; } 244 | 245 | [JsonProperty("metalink-os")] 246 | public string MetalinkOs { get; set; } 247 | 248 | [JsonProperty("metalink-preferred-protocol")] 249 | public string MetalinkPreferredProtocol { get; set; } 250 | 251 | [JsonProperty("metalink-version")] 252 | public string MetalinkVersion { get; set; } 253 | 254 | [JsonProperty("min-split-size")] 255 | public string MinSplitSize { get; set; } 256 | 257 | [JsonProperty("no-file-allocation-limit")] 258 | public string NoFileAllocationLimit { get; set; } 259 | 260 | [JsonProperty("no-netrc")] 261 | public string NoNetrc { get; set; } 262 | 263 | [JsonProperty("no-proxy")] 264 | public string NoProxy { get; set; } 265 | 266 | [JsonProperty("out")] 267 | public string Out { get; set; } 268 | 269 | [JsonProperty("parameterized-uri")] 270 | public string ParameterizedUri { get; set; } 271 | 272 | [JsonProperty("pause")] 273 | public string Pause { get; set; } 274 | 275 | [JsonProperty("pause-metadata")] 276 | public string PauseMetadata { get; set; } 277 | 278 | [JsonProperty("piece-length")] 279 | public string PieceLength { get; set; } 280 | 281 | [JsonProperty("proxy-method")] 282 | public string ProxyMethod { get; set; } 283 | 284 | [JsonProperty("realtime-chunk-checksum")] 285 | public string RealtimeChunkChecksum { get; set; } 286 | 287 | [JsonProperty("referer")] 288 | public string Referer { get; set; } 289 | 290 | [JsonProperty("remote-time")] 291 | public string RemoteTime { get; set; } 292 | 293 | [JsonProperty("remove-control-file")] 294 | public string RemoveControlFile { get; set; } 295 | 296 | [JsonProperty("retry-wait")] 297 | public string RetryWait { get; set; } 298 | 299 | [JsonProperty("reuse-uri")] 300 | public string ReuseUri { get; set; } 301 | 302 | [JsonProperty("rpc-save-upload-metadata")] 303 | public string RpcSaveUploadMetadata { get; set; } 304 | 305 | [JsonProperty("seed-ratio")] 306 | public string SeedRatio { get; set; } 307 | 308 | [JsonProperty("seed-time")] 309 | public string SeedTime { get; set; } 310 | 311 | [JsonProperty("select-file")] 312 | public string SelectFile { get; set; } 313 | 314 | [JsonProperty("split")] 315 | public string Split { get; set; } 316 | 317 | [JsonProperty("ssh-host-key-md")] 318 | public string SshHostKeyMd { get; set; } 319 | 320 | [JsonProperty("stream-piece-selector")] 321 | public string StreamPieceSelector { get; set; } 322 | 323 | [JsonProperty("timeout")] 324 | public string Timeout { get; set; } 325 | 326 | [JsonProperty("uri-selector")] 327 | public string UriSelector { get; set; } 328 | 329 | [JsonProperty("use-head")] 330 | public string UseHead { get; set; } 331 | 332 | [JsonProperty("user-agent")] 333 | public string UserAgent { get; set; } 334 | } 335 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | 6 | namespace AriaNet.Attributes 7 | { 8 | public class ServerDetail 9 | { 10 | [JsonProperty("currentUri")] 11 | public string CurrentUri { get; set; } 12 | 13 | [JsonProperty("downloadSpeed")] 14 | public string DownloadSpeed { get; set; } 15 | 16 | [JsonProperty("uri")] 17 | public string Uri { get; set; } 18 | } 19 | 20 | public class AriaServer 21 | { 22 | [JsonProperty("index")] 23 | public string Index { get; set; } 24 | 25 | [JsonProperty("servers")] 26 | public List Servers { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaSession.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace AriaNet.Attributes 4 | { 5 | [JsonObject] 6 | public class AriaSession 7 | { 8 | [JsonProperty("sessionId")] 9 | public string SessionId { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace AriaNet.Attributes 6 | { 7 | public class AriaStatus 8 | { 9 | 10 | [JsonProperty("bitfield")] 11 | public string Bitfield { get; set; } 12 | 13 | [JsonProperty("completedLength")] 14 | public string CompletedLength { get; set; } 15 | 16 | [JsonProperty("connections")] 17 | public string Connections { get; set; } 18 | 19 | [JsonProperty("dir")] 20 | public string Dir { get; set; } 21 | 22 | [JsonProperty("downloadSpeed")] 23 | public string DownloadSpeed { get; set; } 24 | 25 | [JsonProperty("files")] 26 | public List Files { get; set; } 27 | 28 | [JsonProperty("gid")] 29 | public string TaskId { get; set; } 30 | 31 | [JsonProperty("numPieces")] 32 | public string NumPieces { get; set; } 33 | 34 | [JsonProperty("pieceLength")] 35 | public string PieceLength { get; set; } 36 | 37 | [JsonProperty("status")] 38 | public string Status { get; set; } 39 | 40 | [JsonProperty("totalLength")] 41 | public string TotalLength { get; set; } 42 | 43 | [JsonProperty("uploadLength")] 44 | public string UploadLength { get; set; } 45 | 46 | [JsonProperty("uploadSpeed")] 47 | public string UploadSpeed { get; set; } 48 | 49 | [JsonProperty("infoHash")] 50 | public string InfoHash { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaTorrent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace AriaNet.Attributes 6 | { 7 | public class AriaTorrent 8 | { 9 | [JsonProperty("amChoking")] 10 | public string AmChoking { get; set; } 11 | 12 | [JsonProperty("bitfield")] 13 | public string BitField { get; set; } 14 | 15 | [JsonProperty("downloadSpeed")] 16 | public string DownloadSpeed { get; set; } 17 | 18 | [JsonProperty("ip")] 19 | public string Ip { get; set; } 20 | 21 | [JsonProperty("peerChoking")] 22 | public string PeerChoking { get; set; } 23 | 24 | [JsonProperty("peerId")] 25 | public string PeerId { get; set; } 26 | 27 | [JsonProperty("port")] 28 | public string Port { get; set; } 29 | 30 | [JsonProperty("seeder")] 31 | public string Seeder { get; set; } 32 | 33 | [JsonProperty("uploadSpeed")] 34 | public string UploadSpeed { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaUri.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace AriaNet.Attributes 6 | { 7 | public class AriaUri 8 | { 9 | [JsonProperty("status")] 10 | public string Status { get; set; } 11 | 12 | [JsonProperty("uri")] 13 | public string Uri { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2/AriaVersionInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace AriaNet.Attributes 5 | { 6 | [JsonObject] 7 | public class AriaVersionInfo 8 | { 9 | [JsonProperty("enabledFeatures")] 10 | public List EnabledFeatures { get; set; } 11 | 12 | [JsonProperty("version")] 13 | public string Version { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/Aria2Downloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using AriaNet; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Logging; 9 | using ResourceMonitor.Models.Downloader; 10 | using ResourceMonitor.Services.Declaration; 11 | 12 | namespace ResourceMonitor.Services.Implementation 13 | { 14 | public class Aria2Downloader : IDownloader 15 | { 16 | private readonly ILogger _logger; 17 | private readonly AriaManager _manager; 18 | 19 | public Aria2Downloader(IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger logger) 20 | { 21 | _logger = logger; 22 | _manager = new AriaManager(configuration["Aria2:Url"], 23 | configuration["Aria2:Token"], 24 | httpClientFactory.CreateClient()); 25 | } 26 | 27 | public async Task TryConnect() 28 | { 29 | _logger.LogInformation("尝试连接到 Aria2"); 30 | try 31 | { 32 | var ver = await _manager.GetVersion(); 33 | _logger.LogInformation( 34 | $"连接到 Aria2 成功。版本:{ver.Version}。已启用的特性:{string.Join(", ", ver.EnabledFeatures)}"); 35 | return true; 36 | } 37 | catch (Exception ex) 38 | { 39 | _logger.LogWarning(ex, "连接到 Aria2 失败"); 40 | return false; 41 | } 42 | } 43 | 44 | public async Task> GetAllTasks() 45 | { 46 | _logger.LogDebug("获取 Aria2 所有任务的状态"); 47 | var status = await _manager.GetAllStatus(); 48 | return status.Select(s => new DownloaderTask 49 | { 50 | Id = s.InfoHash, 51 | Magnet = s.InfoHash 52 | }).ToList(); 53 | } 54 | 55 | public async Task IfTaskExists(string id, string url) 56 | { 57 | var allTasks = await GetAllTasks(); 58 | return allTasks.Any(task => task.Id.Equals(id, StringComparison.InvariantCultureIgnoreCase)); 59 | } 60 | 61 | public async Task AddNewTorrentTask(byte[] torrentBytes) 62 | { 63 | var gid = await _manager.AddTorrent(torrentBytes); 64 | await _manager.SaveSession(); 65 | _logger.LogInformation($"向 Aria2 添加任务成功。gid={gid}"); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/CheckNewResourcesBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | using ResourceMonitor.Models.DandanplayApi; 12 | using ResourceMonitor.Models.ResApi; 13 | using ResourceMonitor.Services.Declaration; 14 | 15 | namespace ResourceMonitor.Services.Implementation 16 | { 17 | /// 18 | /// 检查新的资源 19 | /// 20 | public class CheckNewResourcesBackgroundService : BackgroundService 21 | { 22 | private readonly IRulesContainer _rulesContainer; 23 | private readonly IResApi _resApi; 24 | private readonly IDownloader _downloader; 25 | private readonly ITorrentService _torrentService; 26 | private readonly IConfiguration _configuration; 27 | private readonly ILogger _logger; 28 | private readonly int _checkPeriod; 29 | 30 | public CheckNewResourcesBackgroundService(IRulesContainer rulesContainer, 31 | IResApi resApi, IDownloader downloader, ITorrentService torrentService, 32 | IConfiguration configuration, 33 | ILogger logger) 34 | { 35 | _rulesContainer = rulesContainer; 36 | _resApi = resApi; 37 | _downloader = downloader; 38 | _torrentService = torrentService; 39 | _configuration = configuration; 40 | _logger = logger; 41 | _checkPeriod = int.Parse(configuration["CheckPeriod"]); 42 | } 43 | 44 | 45 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 46 | { 47 | _logger.LogInformation($"[检查新资源] 开始在后台运行"); 48 | 49 | while (!stoppingToken.IsCancellationRequested) 50 | { 51 | _logger.LogInformation("开始解析所有规则"); 52 | 53 | //等待规则同步完毕 54 | while (_rulesContainer.IsUpdating) 55 | { 56 | _logger.LogInformation("自动下载规则正在同步中,等待同步完毕..."); 57 | await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); 58 | } 59 | 60 | //按顺序解析每个规则 61 | var ruleList = _rulesContainer.LocalRules; 62 | foreach (var rule in ruleList) 63 | { 64 | await DownloadRule(rule); 65 | } 66 | 67 | //每 _checkPeriod 分钟检查一次 68 | _logger.LogInformation($"全部规则解析完毕,等待 {_checkPeriod} 分钟后再次执行"); 69 | await Task.Delay(TimeSpan.FromMinutes(_checkPeriod), stoppingToken); 70 | } 71 | 72 | _logger.LogInformation("[检查新资源] 结束运行"); 73 | } 74 | 75 | private async Task DownloadRule(AutoDownloadRule rule) 76 | { 77 | _logger.LogInformation($"正在解析规则 {rule}"); 78 | //获得需要下载的新资源 79 | ResourceList resourceList; 80 | try 81 | { 82 | resourceList = await _resApi.List(rule.keyword, rule.subgroupId, rule.typeId); 83 | } 84 | catch (Exception ex) 85 | { 86 | _logger.LogWarning(ex, $"解析规则失败:{rule}"); 87 | return; 88 | } 89 | 90 | //如果设置了 startTime (只下载指定时间之后的资源) 91 | var filtered = resourceList.Resources 92 | .Where(res => rule.startTime == null || res.PublishDateTime >= rule.startTime) 93 | .ToArray(); 94 | 95 | //进一步根据条件过滤 96 | filtered = FilterResources(filtered, rule.chooseNewerIfDuplicate, rule.limitFileSize, 97 | _configuration["LogDetails"] == "true"); 98 | //获取前 n 个结果 99 | filtered = filtered.Take(rule.maxCount).ToArray(); 100 | 101 | foreach (var resourceInfo in filtered) 102 | { 103 | try 104 | { 105 | //对比当前已有的下载任务,只下载新的资源 106 | var magnet = _torrentService.NormalizeMagnetUrl(resourceInfo.Magnet); 107 | var hash = _torrentService.GetHash(magnet); 108 | bool exists = await _downloader.IfTaskExists(hash, magnet); 109 | if (exists) 110 | { 111 | _logger.LogDebug($"任务已经存在,跳过创建 {resourceInfo.Title} {hash}"); 112 | continue; 113 | } 114 | 115 | //解析磁力链至种子文件 116 | var torrentBytes = await _torrentService.DownloadTorrent(magnet); 117 | 118 | if (!_torrentService.IsTorrentFileValid(torrentBytes)) 119 | { 120 | _logger.LogWarning($"解析磁力链得到的种子文件无效 {magnet}"); 121 | continue; 122 | } 123 | 124 | if (torrentBytes == null || torrentBytes.Length <= 0) 125 | { 126 | _logger.LogWarning($"解析磁力链失败 {magnet}"); 127 | continue; 128 | } 129 | 130 | //连接下载器,添加远程任务 131 | await _downloader.AddNewTorrentTask(torrentBytes); 132 | } 133 | catch (Exception ex) 134 | { 135 | _logger.LogWarning(ex, $"向下载器添加任务失败 {resourceInfo.Title}"); 136 | } 137 | } 138 | } 139 | 140 | /// 141 | /// 根据规则过滤资源列表 142 | /// 143 | private ResourceInfo[] FilterResources(IEnumerable resources, bool chooseNewerIfDuplicate, 144 | int limitFileSize, bool logDetails = false) 145 | { 146 | if (resources == null) return new ResourceInfo[0]; 147 | var filtered = resources.ToList(); 148 | if (chooseNewerIfDuplicate) 149 | { 150 | filtered = filtered.GroupBy(f => f.Title) 151 | .Select(g => g.ToArray()) 152 | .Select(gArr => gArr.OrderByDescending(res => res.PublishDateTime).First()) 153 | .ToList(); 154 | if (logDetails) 155 | { 156 | foreach (var info in filtered) 157 | { 158 | _logger.LogDebug($"Filtered By Duplicate. title={info.Title} magnet={info.Magnet}"); 159 | } 160 | } 161 | 162 | //处理 v2 v3 的问题 163 | var duplicated = new List(); 164 | foreach (var resource in filtered) 165 | { 166 | if (duplicated.Contains(resource)) continue; 167 | var normalizedTitle = 168 | Regex.Replace(resource.Title, @"\W|v3|v2", string.Empty, RegexOptions.IgnoreCase); 169 | if (resource.Title.Contains("v3") || resource.Title.Contains("V3")) 170 | { 171 | var replacements = filtered.Where(res => 172 | res.Title != resource.Title && 173 | normalizedTitle == 174 | Regex.Replace(res.Title, @"\W|v2", string.Empty, RegexOptions.IgnoreCase)); 175 | duplicated.AddRange(replacements); 176 | } 177 | else if (resource.Title.Contains("v2") || resource.Title.Contains("V2")) 178 | { 179 | var replacements = filtered.Where(res => 180 | res.Title != resource.Title && 181 | normalizedTitle == Regex.Replace(res.Title, @"\W", string.Empty, RegexOptions.IgnoreCase)); 182 | duplicated.AddRange(replacements); 183 | } 184 | } 185 | 186 | filtered = filtered.Except(duplicated).ToList(); 187 | 188 | if (logDetails) 189 | { 190 | foreach (var info in filtered) 191 | { 192 | _logger.LogDebug($"Filtered By V2V3Duplicate. title={info.Title} magnet={info.Magnet}"); 193 | } 194 | } 195 | } 196 | 197 | if (limitFileSize > 0) 198 | { 199 | filtered = filtered.Where(res => res.FileSizeInMB < limitFileSize).ToList(); 200 | if (logDetails) 201 | { 202 | foreach (var info in filtered) 203 | { 204 | _logger.LogDebug( 205 | $"Filtered By LimitFileSize {limitFileSize}. title={info.Title} size={info.FileSizeInMB} magnet={info.Magnet}"); 206 | } 207 | } 208 | } 209 | 210 | 211 | filtered = filtered.OrderByDescending(res => res.PublishDateTime).ToList(); 212 | if (logDetails) 213 | { 214 | foreach (var info in filtered) 215 | { 216 | _logger.LogDebug( 217 | $"Filtered By PublishDate. title={info.Title} date={info.PublishDateTime} magnet={info.Magnet}"); 218 | } 219 | } 220 | 221 | return filtered.ToArray(); 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/RulesContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Immutable; 3 | using Microsoft.Extensions.Logging; 4 | using ResourceMonitor.Models.DandanplayApi; 5 | using ResourceMonitor.Services.Declaration; 6 | 7 | namespace ResourceMonitor.Services.Implementation 8 | { 9 | public class RulesContainer : IRulesContainer 10 | { 11 | private readonly ILogger _logger; 12 | private readonly ConcurrentDictionary _localRules = new ConcurrentDictionary(); 13 | private bool _isUpdating = true; 14 | 15 | public RulesContainer(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | public bool IsUpdating 21 | { 22 | get => _isUpdating; 23 | set 24 | { 25 | _logger.LogDebug($"RulesContainer.IsUpdating 设置为 {value}"); 26 | _isUpdating = value; 27 | } 28 | } 29 | 30 | public ImmutableList LocalRules => _localRules.Values.ToImmutableList(); 31 | 32 | public void SyncWithServerRules(AutoDownloadRuleListResponse serverRules) 33 | { 34 | _logger.LogInformation("开始同步服务器规则"); 35 | //移除服务器端已经删除的规则 36 | foreach (var removedRuleId in serverRules.removedRuleIds ?? new string[0]) 37 | { 38 | _localRules.TryRemove(removedRuleId, out var removedRule); 39 | _logger.LogInformation($"删除规则 {removedRule}"); 40 | } 41 | 42 | //添加或更新规则 43 | foreach (var serverRule in serverRules.rules) 44 | { 45 | //新增规则 46 | if (!_localRules.ContainsKey(serverRule.id)) 47 | { 48 | _localRules.TryAdd(serverRule.id, serverRule); 49 | _logger.LogInformation($"新增规则 {serverRule}"); 50 | } 51 | else //更新规则 52 | { 53 | var localRule = _localRules[serverRule.id]; 54 | if (serverRule.version > localRule.version) 55 | { 56 | _localRules[serverRule.id] = serverRule; 57 | _logger.LogInformation($"更新规则-原规则 {localRule}"); 58 | _logger.LogInformation($"更新规则-替换为 {serverRule}"); 59 | } 60 | } 61 | } 62 | 63 | _logger.LogInformation($"规则同步完毕,当前本地规则共有 {_localRules.Count} 个"); 64 | } 65 | 66 | } 67 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/SyncRulesBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | using Refit; 11 | using ResourceMonitor.Models.DandanplayApi; 12 | using ResourceMonitor.Services.Declaration; 13 | 14 | namespace ResourceMonitor.Services.Implementation 15 | { 16 | /// 17 | /// 后台同步最新的下载规则 18 | /// 19 | public class SyncRulesBackgroundService : BackgroundService 20 | { 21 | private int _executionCount = 0; 22 | private readonly IDandanplayApi _dandanplayApi; 23 | private readonly IRulesContainer _rulesContainer; 24 | private readonly IConfiguration _configuration; 25 | private readonly ILogger _logger; 26 | private LoginResponse _lastLoginResponse; //上次登录成功的请求,包含 jwt token 和 token 的过期时间 27 | private readonly int _syncPeriod; 28 | 29 | public SyncRulesBackgroundService(IDandanplayApi dandanplayApi, 30 | IRulesContainer rulesContainer, 31 | IConfiguration configuration, 32 | ILogger logger) 33 | { 34 | _dandanplayApi = dandanplayApi; 35 | _rulesContainer = rulesContainer; 36 | _configuration = configuration; 37 | _logger = logger; 38 | _syncPeriod = int.Parse(configuration["SyncPeriod"]); 39 | } 40 | 41 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 42 | { 43 | _logger.LogInformation("[规则同步] 开始运行."); 44 | 45 | while (!stoppingToken.IsCancellationRequested) 46 | { 47 | _rulesContainer.IsUpdating = true; 48 | bool success = await DoWork(); 49 | _rulesContainer.IsUpdating = false; 50 | //每 syncPeriod 分钟运行一次 51 | _logger.LogInformation($"[规则同步] 在后台运行完成,等待 {_syncPeriod} 分钟后重新执行。此次运行结果为:{success}"); 52 | await Task.Delay(TimeSpan.FromMinutes(_syncPeriod), stoppingToken); 53 | } 54 | 55 | _logger.LogInformation("因触发取消,[规则同步] 终止运行."); 56 | } 57 | 58 | private async Task DoWork() 59 | { 60 | //增加计数器 61 | var count = Interlocked.Increment(ref _executionCount); 62 | _logger.LogInformation($"[规则同步] 在后台第 {count} 次运行"); 63 | 64 | //之前登录过,先尝试直接刷新 jwt token 65 | if (_lastLoginResponse != null && _lastLoginResponse.tokenExpireTime >= DateTime.UtcNow) 66 | { 67 | try 68 | { 69 | _lastLoginResponse = await _dandanplayApi.Renew("Bearer " + _lastLoginResponse.token); 70 | } 71 | catch (ApiException ex) 72 | { 73 | _logger.LogDebug(ex, "尝试刷新 jwt token 失败"); 74 | _lastLoginResponse = null; 75 | } 76 | } 77 | 78 | //尝试登录,填充登录参数 79 | if (_lastLoginResponse == null) 80 | { 81 | //读取 appsettings 中的用户名、密码、AppID、AppSecret等信息 82 | var userName = _configuration["Api:UserName"]; 83 | var password = _configuration["Api:Password"]; 84 | var appId = _configuration["Api:AppId"]; 85 | var appSecret = _configuration["Api:AppSecret"]; 86 | 87 | var loginRequest = new LoginRequest 88 | { 89 | userName = userName, 90 | password = password, 91 | appId = appId, 92 | unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() 93 | }; 94 | loginRequest.hash = CalculateLoginRequestHash(loginRequest, appSecret); 95 | try 96 | { 97 | var loginResponse = await _dandanplayApi.Login(loginRequest); 98 | 99 | //先判断是否登录失败了 100 | if (loginResponse.registerRequired || !loginResponse.success || 101 | string.IsNullOrWhiteSpace(loginResponse.token)) 102 | { 103 | _logger.LogWarning($"登录失败,原因: {loginResponse.errorCode} {loginResponse.errorMessage}"); 104 | return false; 105 | } 106 | 107 | //存储 jwt token 108 | _lastLoginResponse = loginResponse; 109 | } 110 | catch (ApiException ex) 111 | { 112 | _logger.LogDebug(ex, "尝试登录失败"); 113 | return false; 114 | } 115 | 116 | } 117 | 118 | _logger.LogInformation($"当前用户使用 ResMonitor 的时限为: 北京时间 {_lastLoginResponse.privileges?.resmonitor:yyyy-MM-dd HH:mm:ss}"); 119 | 120 | //尝试同步下载规则 121 | //获取服务器上当前用户最新版本的规则 122 | var serverRules = await _dandanplayApi.SyncAutoDownloadRules(new AutoDownloadRuleSyncRequest 123 | { 124 | currentRuleIds = _rulesContainer.LocalRules.Select(r => r.id).ToArray() 125 | }, "Bearer " + _lastLoginResponse.token); 126 | //合并本地和服务器端的规则 127 | _rulesContainer.SyncWithServerRules(serverRules); 128 | 129 | return true; 130 | } 131 | 132 | /// 133 | /// 计算登录参数中的 hash 参数 134 | /// 135 | public string CalculateLoginRequestHash(LoginRequest loginRequest, string appSecret) 136 | { 137 | //算法参考 https://api.acplay.net/swagger/ui/index#!/Auth/Auth_Login 138 | var concat = loginRequest.appId + loginRequest.password + loginRequest.unixTimestamp + 139 | loginRequest.userName + appSecret; 140 | using var md5 = MD5.Create(); 141 | var byteArray = md5.ComputeHash(Encoding.UTF8.GetBytes(concat)); 142 | var sb = new StringBuilder(32); 143 | foreach (var b in byteArray) 144 | { 145 | sb.AppendFormat("{0:X2}", b); 146 | } 147 | return sb.ToString(); 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/TorrentService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using MonoTorrent; 6 | using ResourceMonitor.Services.Declaration; 7 | 8 | namespace ResourceMonitor.Services.Implementation 9 | { 10 | public class TorrentService : ITorrentService 11 | { 12 | private readonly IMagnetApi _magnetApi; 13 | private readonly ILogger _logger; 14 | 15 | public TorrentService(IMagnetApi magnetApi, ILogger logger) 16 | { 17 | _magnetApi = magnetApi; 18 | _logger = logger; 19 | } 20 | 21 | public bool IsTorrentFileValid(byte[] torrentBytes) 22 | { 23 | return Torrent.TryLoad(torrentBytes, out var _); 24 | } 25 | 26 | public string NormalizeMagnetUrl(string magnet) 27 | { 28 | return "magnet:?xt=urn:btih:" + GetHash(magnet); 29 | } 30 | 31 | public string GetHash(string magnet) 32 | { 33 | var match = Regex.Match(magnet, @"\w{40}", RegexOptions.IgnoreCase); 34 | if (match.Success) 35 | { 36 | return InfoHash.FromHex(match.Value).ToHex().ToLowerInvariant(); 37 | } 38 | match = Regex.Match(magnet, @"\w{32}", RegexOptions.IgnoreCase); 39 | if (match.Success) 40 | { 41 | return InfoHash.FromBase32(match.Value).ToHex().ToLowerInvariant(); 42 | } 43 | throw new ArgumentException(magnet); 44 | } 45 | 46 | public async Task DownloadTorrent(string magnet) 47 | { 48 | try 49 | { 50 | var content = await _magnetApi.ParseMagnet(magnet); 51 | var torrent = await content.ReadAsByteArrayAsync(); 52 | return torrent; 53 | } 54 | catch (Exception ex) 55 | { 56 | _logger.LogDebug(ex, "下载种子文件时出错"); 57 | return null; 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Services/Implementation/TransmissionDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Logging; 7 | using ResourceMonitor.Models.Downloader; 8 | using ResourceMonitor.Services.Declaration; 9 | using Transmission.API.RPC; 10 | using Transmission.API.RPC.Entity; 11 | 12 | namespace ResourceMonitor.Services.Implementation 13 | { 14 | public class TransmissionDownloader : IDownloader 15 | { 16 | private readonly ILogger _logger; 17 | private readonly Client _client; 18 | 19 | 20 | public TransmissionDownloader(IConfiguration configuration, ILogger logger) 21 | { 22 | _logger = logger; 23 | _client = new Client( 24 | configuration["Transmission:Url"], 25 | Guid.NewGuid().ToString("N"), 26 | configuration["Transmission:Login"], 27 | configuration["Transmission:Password"]); 28 | } 29 | 30 | public async Task TryConnect() 31 | { 32 | _logger.LogInformation("尝试连接到 Transmission"); 33 | try 34 | { 35 | var info = await _client.GetSessionInformationAsync(); 36 | _logger.LogInformation($"连接到 Transmission 成功。版本 {info.Version}。RPC版本 {info.RpcVersion}。"); 37 | return true; 38 | } 39 | catch (Exception ex) 40 | { 41 | _logger.LogWarning(ex, "连接到 Transmission 失败"); 42 | return false; 43 | } 44 | } 45 | 46 | public async Task> GetAllTasks() 47 | { 48 | _logger.LogDebug("获取 Transmission 所有任务的状态"); 49 | var tasks = await _client.TorrentGetAsync(TorrentFields.ALL_FIELDS); 50 | return tasks?.Torrents?.Select(t => new DownloaderTask 51 | { 52 | Id = t.HashString, 53 | Magnet = t.MagnetLink, 54 | Name = t.Name 55 | }).ToList() ?? new List(0); 56 | } 57 | 58 | public async Task IfTaskExists(string id, string url) 59 | { 60 | var allTasks = await GetAllTasks(); 61 | return allTasks.Any(t => t.Id.Equals(id, StringComparison.InvariantCultureIgnoreCase)); 62 | } 63 | 64 | public async Task AddNewTorrentTask(byte[] torrentBytes) 65 | { 66 | var newTorrentInfo = await _client.TorrentAddAsync(new NewTorrent 67 | { 68 | Metainfo = Convert.ToBase64String(torrentBytes), 69 | Paused = false 70 | }); 71 | if (newTorrentInfo == null || newTorrentInfo.ID == 0) 72 | { 73 | throw new Exception("在 Transmission 上建立任务失败"); 74 | } 75 | 76 | _logger.LogInformation($"在 Transmission 上建立任务成功。ID={newTorrentInfo.ID}"); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http.Headers; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.HttpsPolicy; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using Microsoft.Extensions.Logging; 15 | using Refit; 16 | using ResourceMonitor.Services; 17 | using ResourceMonitor.Services.Declaration; 18 | using ResourceMonitor.Services.Implementation; 19 | 20 | namespace ResourceMonitor 21 | { 22 | public class Startup 23 | { 24 | public Startup(IConfiguration configuration) 25 | { 26 | Configuration = configuration; 27 | } 28 | 29 | public IConfiguration Configuration { get; } 30 | 31 | // This method gets called by the runtime. Use this method to add services to the container. 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | services.AddHttpClient(); 35 | switch (Configuration["Downloader"]?.ToLowerInvariant()) 36 | { 37 | case "aria2": 38 | services.AddSingleton(); 39 | break; 40 | case "transmission": 41 | services.AddSingleton(); 42 | break; 43 | } 44 | 45 | //User-Agent: dandanplay/resmonitor 1.2.3.4 46 | var userAgent = string.Format(Configuration["Api:UserAgent"], 47 | Assembly.GetExecutingAssembly().GetName().Version.ToString(4)); 48 | 49 | services.AddRefitClient() 50 | .ConfigureHttpClient(c => 51 | { 52 | c.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); 53 | c.BaseAddress = new Uri(Configuration["Api:ApiBaseUrl"]); 54 | }); 55 | 56 | services.AddRefitClient() 57 | .ConfigureHttpClient(c => 58 | { 59 | c.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); 60 | c.BaseAddress = new Uri(Configuration["Api:ResBaseUrl"]); 61 | }); 62 | 63 | services.AddRefitClient() 64 | .ConfigureHttpClient(c => 65 | { 66 | c.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); 67 | c.BaseAddress = new Uri(Configuration["Api:MagnetBaseUrl"]); 68 | }); 69 | 70 | services.AddSingleton(); 71 | services.AddTransient(); 72 | 73 | services.AddHostedService(); 74 | services.AddHostedService(); 75 | 76 | services.AddControllers().AddNewtonsoftJson(); 77 | } 78 | 79 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 80 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) 81 | { 82 | 83 | logger.LogInformation("初始化配置"); 84 | foreach (var kv in Configuration.AsEnumerable()) 85 | { 86 | logger.LogDebug($"\t{kv.Key}={kv.Value}"); 87 | } 88 | 89 | if (env.IsDevelopment()) 90 | { 91 | app.UseDeveloperExceptionPage(); 92 | } 93 | 94 | app.UseDefaultFiles(); 95 | app.UseStaticFiles(); 96 | 97 | //app.UseHttpsRedirection(); 98 | 99 | app.UseRouting(); 100 | 101 | app.UseAuthorization(); 102 | 103 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "LogDetails": "false", 3 | "AllowedHosts": "*", 4 | "SyncPeriod": 10, 5 | "CheckPeriod": 15, 6 | "Api": { 7 | "ApiBaseUrl": "https://api.dandanplay.net", 8 | "ResBaseUrl": "https://res.dandanplay.net", 9 | "MagnetBaseUrl": "https://m2t.dandanplay.net", 10 | "UserName": "test", 11 | "Password": "test", 12 | "AppId": "resmonitor", 13 | "AppSecret": "PVNFZtQiaKvd24FDBTKFXUT3eJh2iRHA", 14 | "UserAgent": "dandanplay/resmonitor {0}" 15 | }, 16 | "Downloader": "aria2", 17 | "Aria2": { 18 | "Url": "http://localhost:6800/jsonrpc", 19 | "Token": "token" 20 | }, 21 | "Transmission": { 22 | "Url": "http://localhost:9091/transmission/rpc", 23 | "Login": "admin", 24 | "Password": "admin" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 弹弹play资源监视器 6 | 7 | 8 | 9 | 10 |
11 |

弹弹play资源监视器

12 |

程序版本: {{ version }}

13 |

当前下载器: {{ downloader }}

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 |
描述关键词字幕组类型创建时间限定此时间之后限定下载数量去重限定文件体积(MB)
{{ rule.description }}{{ rule.keyword }}{{ rule.subgroupName }} ({{ rule.subgroupId}}){{ rule.typeName }} ({{ rule.typeId }}){{ new Date(rule.createdTime).toLocaleString() }}{{ new Date(rule.startTime).toLocaleString() }}{{ rule.maxCount }}{{ rule.chooseNewerIfDuplicate }}{{ rule.limitFileSize }}
43 |
44 | 66 | 67 | -------------------------------------------------------------------------------- /ResourceMonitor/ResourceMonitor/wwwroot/scripts/jquery-3.5.1.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0