├── .gitignore ├── Emby.Actress ├── Config.cs ├── Emby.Actress.csproj ├── EmbyActressImportService.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── Icon.ico ├── Program.cs └── README.md ├── Emby.Plugins.JavScraper.sln ├── Emby.Plugins.JavScraper ├── Baidu │ ├── BaiduAccessToken.cs │ ├── BaiduApiResult.cs │ ├── BaiduFanyiService.cs │ ├── BaiduServiceBase.cs │ └── BodyAnalysisService.cs ├── Configuration │ ├── ConfigPage.html │ ├── JavOrganizationConfigPage.html │ ├── JavOrganizationOptions.cs │ ├── Jellyfin.ConfigPage.html │ ├── Jellyfin.JavOrganizationConfigPage.html │ └── PluginConfiguration.cs ├── Data │ ├── ApplicationDbContext.cs │ ├── ImageFaceCenterPoint.cs │ ├── Metadata.cs │ ├── Plot.cs │ └── Translation.cs ├── Emby.Plugins.JavScraper.csproj ├── Extensions │ ├── FileExtensionContentTypeProvider.cs │ ├── ILogManagerExtensions.cs │ ├── JellyfinExtensions.cs │ ├── NamedLockerAsync.cs │ ├── PluginExtensions.cs │ └── StringExtensions.cs ├── FixChineseSubtitleGenreTask.cs ├── Http │ ├── HttpClientEx.cs │ ├── JavWebProxy.cs │ └── ProxyHttpClientHandler.cs ├── JavIdRecognizer.cs ├── JavImageProvider.cs ├── JavMovieProvider.cs ├── JavOrganizeTask.cs ├── JavPersonProvider.cs ├── JavPersonTask.cs ├── Plugin.cs ├── Scrapers │ ├── AVSOX.cs │ ├── AbstractScraper.cs │ ├── FC2.cs │ ├── Gfriends.cs │ ├── Jav123.cs │ ├── JavBus.cs │ ├── JavDB.cs │ ├── JavPersonIndex.cs │ ├── JavVideo.cs │ ├── JavVideoIndex.cs │ ├── LevenshteinDistance.cs │ ├── MgsTage.cs │ └── R18.cs ├── Services │ ├── ImageProxyService.cs │ ├── ImageService.cs │ ├── TranslationService.cs │ └── UpdateService.cs └── thumb.png ├── Jellyfin.GenerateConfigurationPage ├── Jellyfin.GenerateConfigurationPage.csproj └── Program.cs ├── README.md ├── Screenshots └── Screenshot01.png ├── cf-worker ├── README.md └── index.js └── manifest.json /.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 | -------------------------------------------------------------------------------- /Emby.Actress/Config.cs: -------------------------------------------------------------------------------- 1 | namespace Emby.Actress 2 | { 3 | /// 4 | /// 5 | /// 6 | public class Config 7 | { 8 | /// 9 | /// Emby 站点 10 | /// 11 | public string url { get; set; } = "http://localhost:8096/"; 12 | 13 | /// 14 | /// Api Key 15 | /// 16 | public string api_key { get; set; } = ""; 17 | 18 | /// 19 | /// 头像文件夹 20 | /// 21 | public string dir { get; set; } = "女优头像"; 22 | } 23 | } -------------------------------------------------------------------------------- /Emby.Actress/Emby.Actress.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1;net461 6 | Icon.ico 7 | Debug;Release;Debug.Jellyfin;Release.Jellyfin 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Emby.Actress/EmbyActressImportService.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Web; 11 | 12 | namespace Emby.Actress 13 | { 14 | public class EmbyActressImportService 15 | { 16 | private HttpClient client; 17 | private Config cfg; 18 | 19 | public EmbyActressImportService(Config cfg) 20 | { 21 | this.cfg = cfg; 22 | client = new HttpClient(); 23 | client.BaseAddress = new Uri($"{cfg.url?.TrimEnd('/')}/emby/"); 24 | } 25 | 26 | private string Get(IEnumerable ls) 27 | { 28 | var sb = new StringBuilder(); 29 | 30 | sb.AppendLine($@" 31 | 32 | {DateTime.Now:yyyy-MM-dd HH:mm} 33 | 全部女优"); 34 | 35 | foreach (var a in ls) 36 | { 37 | sb.AppendLine($@" 38 | {a} 39 | Actor 40 | "); 41 | } 42 | 43 | sb.AppendLine(""); 44 | 45 | return sb.ToString(); 46 | } 47 | 48 | internal async Task StartAsync() 49 | { 50 | var dir = cfg.dir; 51 | var files = Directory.GetFiles(dir, "*.jpg", SearchOption.AllDirectories).Union(Directory.GetFiles(dir, "*.jpeg", SearchOption.AllDirectories)) 52 | .Union(Directory.GetFiles(dir, "*.png", SearchOption.AllDirectories)) 53 | .Select(o => new { name = Path.GetFileNameWithoutExtension(o), file = o }).ToList(); 54 | 55 | if (files.Count == 0) 56 | { 57 | Console.WriteLine($"{Path.GetFileName(dir)} 中没有女优头像。"); 58 | return; 59 | } 60 | Console.WriteLine($"{Path.GetFileName(dir)} 中找到 {files.Count} 个女优头像。"); 61 | 62 | var nfo_name = $"{Path.GetFileNameWithoutExtension(dir)}.nfo"; 63 | var nfo_txt = Get(files.Select(o => o.name)); 64 | 65 | try 66 | { 67 | File.WriteAllText(nfo_name, nfo_txt); 68 | Console.WriteLine($"保存 {nfo_name} 文件成功,如何使用请参阅 https://github.com/JavScraper/Emby.Plugins.JavScraper/Emby.Actress"); 69 | } 70 | catch 71 | { 72 | Console.WriteLine($"保存 {nfo_name} 文件失败。"); 73 | } 74 | 75 | var pesions = await GetPesionsAsync(); 76 | if (pesions == null) 77 | { 78 | Console.WriteLine("查询演职员信息失败,请检查 url 和 api_key 配置是否正确。"); 79 | return; 80 | } 81 | var total = pesions.Count(); 82 | pesions = pesions.Where(o => o.ImageTags?.ContainsKey("Primary") != true) 83 | .ToList(); 84 | 85 | Console.WriteLine($"在 Emby 中找到 {total} 个演职员,其中 {pesions.Count} 个没有头像。"); 86 | 87 | if (pesions.Count == 0) 88 | { 89 | Console.WriteLine("没有演职员需要更新头像。"); 90 | return; 91 | } 92 | 93 | var all = pesions.Join(files, o => o.Name, o => o.name, (o, v) => new { persion = o, file = v }).ToList(); 94 | 95 | if (all.Count == 0) 96 | { 97 | Console.WriteLine("没有匹配的演职员需要更新头像。"); 98 | await SaveMissing(); 99 | return; 100 | } 101 | 102 | int i = 0; 103 | int c = all.Count; 104 | foreach (var a in all) 105 | { 106 | var imageContent = new StringContent(Convert.ToBase64String(File.ReadAllBytes(a.file.file))); 107 | if (a.file.file.EndsWith("png", StringComparison.OrdinalIgnoreCase)) 108 | imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png"); 109 | else 110 | imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); 111 | 112 | var action = $"Items/{a.persion.Id}/Images/Primary"; 113 | i++; 114 | Console.WriteLine($"{i}/{c} {i * 1.0 / c:p} {a.persion.Name}"); 115 | try 116 | { 117 | var r = await DoPost(action, imageContent); 118 | } 119 | catch (Exception ex) 120 | { 121 | Console.WriteLine($"{a.persion.Name} 更新失败:{ex.Message}"); 122 | } 123 | } 124 | 125 | await SaveMissing(); 126 | } 127 | 128 | private async Task SaveMissing() 129 | { 130 | var dir = cfg.dir; 131 | 132 | var pesions = await GetPesionsAsync(); 133 | if (pesions == null) 134 | { 135 | Console.WriteLine("重新获取演员失败。"); 136 | return; 137 | } 138 | 139 | pesions = pesions.Where(o => o.ImageTags?.ContainsKey("Primary") != true) 140 | .ToList(); 141 | 142 | if (pesions.Count == 0) 143 | { 144 | Console.WriteLine("全部演职员已经有头像了。"); 145 | return; 146 | } 147 | Console.WriteLine($"在 Emby 中找到 {pesions.Count} 个演职员没有头像。"); 148 | 149 | 150 | var missing_name = $"{Path.GetFileNameWithoutExtension(dir)}.Missing.txt"; 151 | 152 | try 153 | { 154 | File.WriteAllText(missing_name, string.Join(Environment.NewLine, pesions.Select(o => o.Name))); 155 | Console.WriteLine($"保存 {missing_name} 文件成功,以上演职员缺少头像。"); 156 | } 157 | catch 158 | { 159 | Console.WriteLine($"保存 {missing_name} 文件失败。"); 160 | } 161 | } 162 | 163 | public async Task> GetPesionsAsync() 164 | { 165 | var ll = await DoGet>("Persons"); 166 | 167 | return ll?.Items; 168 | } 169 | 170 | /// 171 | /// Post 操作 172 | /// 173 | /// 操作 174 | /// 175 | /// 176 | internal Task DoPostAsJson(string action, object model) 177 | where TResult : new() 178 | { 179 | var sp = action?.IndexOf("?") >= 0 ? "&" : "?"; 180 | action = $"{action}{sp}api_key={cfg.api_key}"; 181 | 182 | var task = client.PostAsJsonAsync(action, model); 183 | return DoProcess(task); 184 | } 185 | 186 | /// 187 | /// Post 操作 188 | /// 189 | /// 操作 190 | /// 191 | /// 192 | internal Task DoPost(string action, HttpContent httpContent) 193 | where TResult : new() 194 | { 195 | var sp = action?.IndexOf("?") >= 0 ? "&" : "?"; 196 | action = $"{action}{sp}api_key={cfg.api_key}"; 197 | 198 | var task = client.PostAsync(action, httpContent); 199 | return DoProcess(task); 200 | } 201 | 202 | /// 203 | /// Get 操作 204 | /// 205 | /// 操作 206 | /// 参数 207 | /// 208 | internal Task DoGet(string action, Dictionary param = null) 209 | where TResult : new() 210 | { 211 | if (param == null) 212 | param = new Dictionary(); 213 | param["api_key"] = cfg.api_key; 214 | 215 | var p = string.Join("&", param.Select(o => $"{o.Key}={HttpUtility.UrlEncode(o.Value ?? string.Empty)}")); 216 | var sp = action?.IndexOf("?") >= 0 ? "&" : "?"; 217 | action = $"{action}{sp}{p}"; 218 | 219 | var task = client.GetAsync(action); 220 | return DoProcess(task); 221 | } 222 | 223 | /// 224 | /// HTTP 请求处理 225 | /// 226 | /// The type of the result. 227 | /// The index. 228 | /// The task. 229 | /// 230 | internal async Task DoProcess(Task task) 231 | where TResult : new() 232 | { 233 | string json = null; 234 | try 235 | { 236 | var r = await task; 237 | json = await r.Content.ReadAsStringAsync(); 238 | 239 | if (r.IsSuccessStatusCode == false) 240 | { 241 | return default; 242 | } 243 | return JsonConvert.DeserializeObject(json); 244 | } 245 | catch 246 | { 247 | return default; 248 | } 249 | } 250 | } 251 | 252 | public static class HttpClientExtensions 253 | { 254 | public static async Task PostAsJsonAsync(this HttpClient client, string requestUrl, TModel model) 255 | { 256 | var json = JsonConvert.SerializeObject(model); 257 | var stringContent = new StringContent(json, Encoding.UTF8, "application/json"); 258 | return await client.PostAsync(requestUrl, stringContent); 259 | } 260 | } 261 | 262 | public class EmbyListReault 263 | { 264 | public List Items { get; set; } 265 | public int TotalRecordCount { get; set; } 266 | } 267 | 268 | public class PesionData 269 | { 270 | public string Name { get; set; } 271 | public string ServerId { get; set; } 272 | public string Id { get; set; } 273 | public string Type { get; set; } 274 | public Dictionary ImageTags { get; set; } 275 | public object[] BackdropImageTags { get; set; } 276 | 277 | public override string ToString() 278 | => Name; 279 | } 280 | } -------------------------------------------------------------------------------- /Emby.Actress/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Emby.Actress/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks 13 | 14 | 15 | 16 | 17 | A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. 18 | 19 | 20 | 21 | 22 | A list of unmanaged 32 bit assembly names to include, delimited with line breaks. 23 | 24 | 25 | 26 | 27 | A list of unmanaged 64 bit assembly names to include, delimited with line breaks. 28 | 29 | 30 | 31 | 32 | The order of preloaded assemblies, delimited with line breaks. 33 | 34 | 35 | 36 | 37 | 38 | This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. 39 | 40 | 41 | 42 | 43 | Controls if .pdbs for reference assemblies are also embedded. 44 | 45 | 46 | 47 | 48 | Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. 49 | 50 | 51 | 52 | 53 | As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. 54 | 55 | 56 | 57 | 58 | Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. 59 | 60 | 61 | 62 | 63 | Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. 64 | 65 | 66 | 67 | 68 | A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | 69 | 70 | 71 | 72 | 73 | A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. 74 | 75 | 76 | 77 | 78 | A list of unmanaged 32 bit assembly names to include, delimited with |. 79 | 80 | 81 | 82 | 83 | A list of unmanaged 64 bit assembly names to include, delimited with |. 84 | 85 | 86 | 87 | 88 | The order of preloaded assemblies, delimited with |. 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 97 | 98 | 99 | 100 | 101 | A comma-separated list of error codes that can be safely ignored in assembly verification. 102 | 103 | 104 | 105 | 106 | 'false' to turn off automatic generation of the XML Schema file. 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Emby.Actress/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavScraper/Emby.Plugins.JavScraper/7ef5fbfdb26b3ac61d59a32030da899bb871c029/Emby.Actress/Icon.ico -------------------------------------------------------------------------------- /Emby.Actress/Program.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.Actress 7 | { 8 | internal class Program 9 | { 10 | private static Task Main(string[] args) 11 | { 12 | Task ShowError(string _error) 13 | { 14 | Console.WriteLine(_error); 15 | Console.ReadKey(); 16 | return Task.CompletedTask; 17 | } 18 | var file = "Config.json"; 19 | 20 | Config cfg; 21 | if (File.Exists(file) == false) 22 | { 23 | cfg = new Config(); 24 | File.WriteAllText(file, JsonConvert.SerializeObject(cfg, Formatting.Indented)); 25 | return ShowError($"请使用文本编辑器打开 {file} 文件,配置里面的 Emby url 和 key。"); 26 | } 27 | 28 | var json = File.ReadAllText(file); 29 | try 30 | { 31 | cfg = JsonConvert.DeserializeObject(json); 32 | } 33 | catch 34 | { 35 | return ShowError($"配置文件解析失败,请检查格式是否正确。"); 36 | } 37 | 38 | if (string.IsNullOrWhiteSpace(cfg?.api_key)) 39 | return ShowError($"api_key 不能为空。"); 40 | 41 | if (string.IsNullOrWhiteSpace(cfg.url)) 42 | return ShowError($"emby 的 url 地址不能为空。"); 43 | 44 | try 45 | { 46 | new Uri(cfg.url); 47 | } 48 | catch 49 | { 50 | return ShowError($"{cfg.url} 不是一个有效的 URL 地址。"); 51 | } 52 | 53 | if (string.IsNullOrWhiteSpace(cfg.dir)) 54 | cfg.dir = "."; 55 | 56 | if (Directory.Exists(cfg.dir) == false) 57 | return ShowError($"请创建名为 【{cfg.dir}】 的文件夹,并把女优头像拷贝到里面。"); 58 | 59 | var s = new EmbyActressImportService(cfg); 60 | 61 | return s.StartAsync().ContinueWith(o => Console.ReadKey()); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Emby.Actress/README.md: -------------------------------------------------------------------------------- 1 | # Emby.Actress 2 | Emby 女优头像批量导入工具 3 | 4 | >已经集成头像采集,可以在 **控制台-高级-计划任务** 中找到 **JavScraper: 采集缺失的女优头像**,并点击右边的三角符号开始启动采集任务。 5 | 6 | # 用途 7 | 通过调用 Emby 的接口,将网友收集好的女优头像批量导入到 Emby 中 8 | 9 | # 下载 10 | [点击这里下载](https://github.com/JavScraper/Emby.Plugins.JavScraper/releases/tag/v1.22.27.1109) 11 | ## 文件说明 12 | ### [Emby.Actress@20200202-Windows.zip](https://github.com/JavScraper/Emby.Plugins.JavScraper/releases/download/v1.22.27.1109/Emby.Actress@20200202-Windows.zip) 13 | - 依赖 [.NET Framework 4.6.1](https://support.microsoft.com/zh-cn/help/3102436/the-net-framework-4-6-1-offline-installer-for-windows) 14 | - Windows 用户首选,Windows 7、8、10 都自带有运行库了。 15 | ### [Emby.Actress@20200202-dotnet_core.zip](https://github.com/JavScraper/Emby.Plugins.JavScraper/releases/download/v1.22.27.1109/Emby.Actress@20200202-dotnet_core.zip) 16 | - 依赖 [.NET Core 3.1 Runtime](https://dotnet.microsoft.com/download/dotnet-core/current/runtime) 17 | - Linux/MAC OSX/Windows 可用。 18 | 19 | # 使用 20 | 21 | ## 女优头像获取 22 | ### 自己收集 23 | 自己去网上收集,并保存为 `女优名.jpg` 的名称。支持 `.jpg`、`jpeg`、'png' 三种图片格式。 24 | 25 | ### 使用网友已经收集好的 26 | 点这里 [女优头像](https://github.com/junerain123/javsdt/releases/tag/%E5%A5%B3%E4%BC%98%E5%A4%B4%E5%83%8F) 下载名为 [actors.zip](https://github.com/junerain123/javsdt/releases/download/%E5%A5%B3%E4%BC%98%E5%A4%B4%E5%83%8F/actors.zip) 27 | 的压缩包。 28 | 29 | ## 配置 30 | ### Config.json 文件说明 31 | ```json 32 | { 33 | "url": "http://localhost:8096/", 34 | "api_key": "c976d594ee1f466da82bfd434f481234", 35 | "dir": "女优头像" 36 | } 37 | ``` 38 | - **url** 你自己 Emby 服务器的地址 39 | - **api_key** 点击 右上角的齿轮 **管理 Emby 服务器** - **高级** - **API 密钥** 中添加。 40 | - **dir** 女优头像所在文件夹。 41 | 42 | ## 执行 43 | - Windows 下载双击执行 `Emby.Actress.exe` 即可。 44 | - Linux/Mac 下执行 `dotnet Emby.Actress.dll` 45 | 46 | # 如何一次性导入全部女优 47 | - 导入程序执行的时候会生成一个 `.nfo` 的文件,比如:`女优头像.nfo`。 48 | - 在 `媒体库` 中的 `媒体资料储存方式` 勾选 `Nfo`。 49 | - 在`媒体库所在的文件夹`中,拷贝一个文件体积较小的电影,并重命名与之前的`nfo文件同名`,比如`女优头像.mkv` 50 | - 把`女优头像.nfo`拷贝到和`女优头像.mkv`同一个文件夹下。 51 | - 点击媒体库中的`扫描媒体库文件`,等待在媒体库中找到`女优头像`这部电影,点进去,会发现下面一堆演员。 52 | - 重新运行上面的程序,就可以一次性导入全部的女优了。 -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29613.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Plugins.JavScraper", "Emby.Plugins.JavScraper\Emby.Plugins.JavScraper.csproj", "{F2860322-0D9E-4BB5-A9FD-0369E8DD4682}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E49B9616-42B7-42B1-BAA3-4309CA43B907}" 9 | ProjectSection(SolutionItems) = preProject 10 | manifest.json = manifest.json 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cf-worker", "cf-worker", "{6B97EC77-09C9-4DB6-BA33-8467CE359722}" 15 | ProjectSection(SolutionItems) = preProject 16 | cf-worker\index.js = cf-worker\index.js 17 | cf-worker\README.md = cf-worker\README.md 18 | EndProjectSection 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Actress", "Emby.Actress\Emby.Actress.csproj", "{021E218A-7852-4CE5-96A7-91CB23F9F0F3}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.GenerateConfigurationPage", "Jellyfin.GenerateConfigurationPage\Jellyfin.GenerateConfigurationPage.csproj", "{B348470A-327C-4A31-B1D2-DE92A2EAD9D9}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug.Jellyfin|Any CPU = Debug.Jellyfin|Any CPU 27 | Debug|Any CPU = Debug|Any CPU 28 | Release.Jellyfin|Any CPU = Release.Jellyfin|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Debug.Jellyfin|Any CPU.ActiveCfg = Debug.Jellyfin|Any CPU 33 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Debug.Jellyfin|Any CPU.Build.0 = Debug.Jellyfin|Any CPU 34 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Release.Jellyfin|Any CPU.ActiveCfg = Release.Jellyfin|Any CPU 37 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Release.Jellyfin|Any CPU.Build.0 = Release.Jellyfin|Any CPU 38 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {F2860322-0D9E-4BB5-A9FD-0369E8DD4682}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Debug.Jellyfin|Any CPU.ActiveCfg = Debug.Jellyfin|Any CPU 41 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Debug.Jellyfin|Any CPU.Build.0 = Debug.Jellyfin|Any CPU 42 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Release.Jellyfin|Any CPU.ActiveCfg = Release.Jellyfin|Any CPU 45 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Release.Jellyfin|Any CPU.Build.0 = Release.Jellyfin|Any CPU 46 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {021E218A-7852-4CE5-96A7-91CB23F9F0F3}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Debug.Jellyfin|Any CPU.ActiveCfg = Debug|Any CPU 49 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Debug.Jellyfin|Any CPU.Build.0 = Debug|Any CPU 50 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Release.Jellyfin|Any CPU.ActiveCfg = Release|Any CPU 53 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Release.Jellyfin|Any CPU.Build.0 = Release|Any CPU 54 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {B348470A-327C-4A31-B1D2-DE92A2EAD9D9}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {3D521EEA-625C-49C5-8211-C165082747FD} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Baidu/BaiduAccessToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Baidu.AI 4 | { 5 | /// 6 | /// 百度人脸识别令牌 7 | /// 8 | public class BaiduAccessToken 9 | { 10 | /// 11 | /// Gets or sets the refresh token. 12 | /// 13 | public string refresh_token { get; set; } 14 | 15 | /// 16 | /// Access Token的有效期(秒为单位,一般为1个月); 17 | /// 18 | public int expires_in { get; set; } 19 | 20 | /// 21 | /// Gets or sets the scope. 22 | /// 23 | public string scope { get; set; } 24 | 25 | /// 26 | /// Gets or sets the session key. 27 | /// 28 | public string session_key { get; set; } 29 | 30 | /// 31 | /// 要获取的Access Token 32 | /// 33 | public string access_token { get; set; } 34 | 35 | /// 36 | /// Gets or sets the session secret. 37 | /// 38 | public string session_secret { get; set; } 39 | 40 | /// 41 | /// 错误码;关于错误码的详细信息请参考下方鉴权认证错误码。 42 | /// 43 | public string error { get; set; } 44 | 45 | /// 46 | /// 错误描述信息,帮助理解和解决发生的错误。 47 | /// 48 | public string error_description { get; set; } 49 | 50 | /// 51 | /// 创建时间 52 | /// 53 | public DateTime created { get; set; } = DateTime.Now; 54 | 55 | /// 56 | /// 过期时间 57 | /// 58 | public DateTime expired => created.AddSeconds(expires_in).AddHours(-1); 59 | 60 | /// 61 | /// 是否有效 62 | /// 63 | public bool IsValid => string.IsNullOrWhiteSpace(access_token) == false && expired > DateTime.Now; 64 | } 65 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Baidu/BaiduApiResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Baidu.AI 4 | { 5 | /// 6 | /// 结果 7 | /// 8 | /// 9 | public class BaiduApiResult 10 | { 11 | public int error_code { get; set; } 12 | public string error_msg { get; set; } 13 | public long log_id { get; set; } 14 | public int timestamp { get; set; } 15 | public int cached { get; set; } 16 | public TData result { get; set; } 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The result. 22 | /// if set to true [success]. 23 | public BaiduApiResult(TData result, bool success = true) 24 | { 25 | this.result = result; 26 | error_code = 0; 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | public BaiduApiResult() { } 33 | 34 | /// 35 | /// 错误信息 36 | /// 37 | /// The model. 38 | /// 39 | /// The result of the conversion. 40 | /// 41 | public static implicit operator string(BaiduApiResult model) 42 | { 43 | return model?.error_msg; 44 | } 45 | 46 | /// 47 | /// 错误信息 48 | /// 49 | /// The MSG. 50 | /// 51 | /// The result of the conversion. 52 | /// 53 | public static implicit operator BaiduApiResult(string msg) 54 | { 55 | return new BaiduApiResult() { error_msg = msg, error_code = 1 }; 56 | } 57 | 58 | /// 59 | /// 内容信息 60 | /// 61 | /// The model. 62 | /// 63 | /// The result of the conversion. 64 | /// 65 | public static implicit operator TData(BaiduApiResult model) 66 | { 67 | if (model == null) 68 | return default(TData); 69 | return model.result; 70 | } 71 | 72 | /// 73 | /// 内容信息 74 | /// 75 | /// The t. 76 | /// 77 | /// The result of the conversion. 78 | /// 79 | public static implicit operator BaiduApiResult(TData t) 80 | { 81 | return new BaiduApiResult() { result = t, error_code = 0 }; 82 | } 83 | 84 | /// 85 | /// 模型校验错误信息 86 | /// 87 | /// The ex. 88 | /// 89 | /// The result of the conversion. 90 | /// 91 | public static implicit operator BaiduApiResult(Exception ex) 92 | { 93 | return new BaiduApiResult() 94 | { 95 | error_msg = ex.Message, 96 | error_code = 1, 97 | }; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Baidu/BaiduFanyiService.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Serialization; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Emby.Plugins.JavScraper.Baidu 10 | { 11 | /// 12 | /// 百度翻译 13 | /// 14 | public static class BaiduFanyiService 15 | { 16 | private static HttpClient client = new HttpClient() { BaseAddress = new Uri("http://api.fanyi.baidu.com/api/trans/vip/translate") }; 17 | private static Random rd = new Random(); 18 | 19 | public static async Task Fanyi(string q, IJsonSerializer jsonSerializer) 20 | { 21 | if (Plugin.Instance.Configuration.EnableBaiduFanyi == false) 22 | return null; 23 | 24 | // 源语言 25 | string from = "auto"; 26 | // 目标语言 27 | string to = Plugin.Instance.Configuration.BaiduFanyiLanguage?.Trim(); 28 | if (string.IsNullOrWhiteSpace(to)) 29 | to = "zh"; 30 | 31 | string appId = Plugin.Instance.Configuration.BaiduFanyiApiKey; 32 | string secretKey = Plugin.Instance.Configuration.BaiduFanyiSecretKey; 33 | 34 | string salt = rd.Next(100000).ToString(); 35 | string sign = EncryptString(appId + q + salt + secretKey); 36 | 37 | var param = new Dictionary() 38 | { 39 | ["q"] = q, 40 | ["from"] = from, 41 | ["to"] = to, 42 | ["appid"] = appId, 43 | ["salt"] = salt, 44 | ["sign"] = sign, 45 | }; 46 | 47 | var resp = await client.PostAsync("", new FormUrlEncodedContent(param)); 48 | 49 | if (resp.IsSuccessStatusCode == false) 50 | return null; 51 | 52 | var json = await resp.Content.ReadAsStringAsync(); 53 | 54 | return jsonSerializer.DeserializeFromString(json); 55 | } 56 | 57 | /// 58 | /// 计算MD5值 59 | /// 60 | /// 61 | /// 62 | private static string EncryptString(string str) 63 | { 64 | using (MD5 md5 = MD5.Create()) 65 | { 66 | // 将字符串转换成字节数组 67 | byte[] byteOld = Encoding.UTF8.GetBytes(str); 68 | // 调用加密方法 69 | byte[] byteNew = md5.ComputeHash(byteOld); 70 | return BitConverter.ToString(byteNew).Replace("-", "").ToLower(); 71 | } 72 | } 73 | } 74 | 75 | /// 76 | /// 翻译结果 77 | /// 78 | public class BaiduFanyiResult 79 | { 80 | /// 81 | /// 来源语言 82 | /// 83 | public string from { get; set; } 84 | 85 | /// 86 | /// 目标语言 87 | /// 88 | public string to { get; set; } 89 | 90 | /// 91 | /// 翻译内容 92 | /// 93 | public List trans_result { get; set; } 94 | } 95 | 96 | /// 97 | /// 翻译内容 98 | /// 99 | public class BaiduFanyiTransResult 100 | { 101 | public string src { get; set; } 102 | public string dst { get; set; } 103 | } 104 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Baidu/BaiduServiceBase.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Serialization; 2 | using System; 3 | using System.Collections.Generic; 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; 10 | using System.Threading.Tasks; 11 | 12 | namespace Baidu.AI 13 | { 14 | /// 15 | /// 基础服务 16 | /// 17 | public abstract class BaiduServiceBase 18 | { 19 | /// 20 | /// ApiKey 21 | /// 22 | public string ApiKey { get; } 23 | 24 | /// 25 | /// SecretKey 26 | /// 27 | public string SecretKey { get; } 28 | 29 | private BaiduAccessToken token; 30 | protected HttpClient client; 31 | private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); 32 | private readonly IJsonSerializer jsonSerializer; 33 | 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | protected BaiduServiceBase(string apiKey, string secretKey, IJsonSerializer jsonSerializer) 40 | { 41 | ApiKey = apiKey; 42 | SecretKey = secretKey; 43 | this.jsonSerializer = jsonSerializer; 44 | client = new HttpClient(); 45 | client.DefaultRequestHeaders.Accept.Clear(); 46 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 47 | } 48 | 49 | /// 50 | /// 获取访问Token 51 | /// 52 | public async Task GetAccessTokenAsync(bool force = false) 53 | { 54 | await semaphoreSlim.WaitAsync(); 55 | try 56 | { 57 | if (force == false && token?.IsValid == true) 58 | return token; 59 | 60 | var dic = new Dictionary() 61 | { 62 | ["grant_type"] = "client_credentials", 63 | ["client_id"] = ApiKey, 64 | ["client_secret"] = SecretKey 65 | }; 66 | 67 | var resp = await client.PostAsync("https://aip.baidubce.com/oauth/2.0/token", new FormUrlEncodedContent(dic)); 68 | if (resp.IsSuccessStatusCode == true) 69 | { 70 | var json = await resp.Content.ReadAsStringAsync(); 71 | token = jsonSerializer.DeserializeFromString(json); 72 | if (token != null) 73 | token.created = DateTime.Now; 74 | } 75 | return token; 76 | } 77 | finally 78 | { 79 | semaphoreSlim.Release(); 80 | } 81 | } 82 | 83 | /// 84 | /// POST 请求 85 | /// 86 | /// Type of the result. 87 | /// URL of the resource. 88 | /// The parameter. 89 | /// 90 | /// An asynchronous result that yields an ApiResult<BaiduFaceApiReault<TResult>> 91 | /// 92 | public async Task> DoPost(string url, object param) 93 | { 94 | var token = await GetAccessTokenAsync(); 95 | if (token == null) 96 | return "令牌不正确。"; 97 | var s = url.IndexOf('?') > 0 ? "&" : "?"; 98 | url = $"{url}{s}access_token={token.access_token}"; 99 | try 100 | { 101 | var json = jsonSerializer.SerializeToString(param); 102 | 103 | var resp = await client.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json")); 104 | 105 | if (resp.IsSuccessStatusCode) 106 | { 107 | json = await resp.Content.ReadAsStringAsync(); 108 | return jsonSerializer.DeserializeFromString>(json); 109 | } 110 | 111 | return $"请求出错:{resp.ReasonPhrase}"; 112 | } 113 | catch (Exception ex) 114 | { 115 | return $"请求出错:{ex.Message}"; 116 | } 117 | } 118 | 119 | /// 120 | /// POST 请求 121 | /// 122 | /// Type of the result. 123 | /// URL of the resource. 124 | /// The parameter. 125 | /// 126 | /// An asynchronous result that yields an ApiResult<BaiduFaceApiReault<TResult>> 127 | /// 128 | public async Task DoPostForm(string url, Dictionary nv) 129 | { 130 | var token = await GetAccessTokenAsync(); 131 | if (token == null) 132 | return default; 133 | var s = url.IndexOf('?') > 0 ? "&" : "?"; 134 | url = $"{url}{s}access_token={token.access_token}"; 135 | try 136 | { 137 | var str = string.Join("&", nv.Select(o => $"{o.Key}={WebUtility.UrlEncode(o.Value)}")); 138 | var content = new StringContent(str, Encoding.UTF8, "application/x-www-form-urlencoded"); 139 | var resp = await client.PostAsync(url, content); 140 | 141 | if (resp.IsSuccessStatusCode) 142 | { 143 | var json = await resp.Content.ReadAsStringAsync(); 144 | return jsonSerializer.DeserializeFromString(json); 145 | } 146 | 147 | return default; 148 | } 149 | catch 150 | { 151 | return default; 152 | } 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Baidu/BodyAnalysisService.cs: -------------------------------------------------------------------------------- 1 | using Baidu.AI; 2 | using MediaBrowser.Model.Serialization; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | 8 | namespace Emby.Plugins.JavScraper.Baidu 9 | { 10 | /// 11 | /// 百度人体分析 12 | /// https://ai.baidu.com/ai-doc/BODY/0k3cpyxme 13 | /// 14 | public class BodyAnalysisService : BaiduServiceBase 15 | { 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | public BodyAnalysisService(string apiKey, string secretKey, IJsonSerializer jsonSerializer) 23 | : base(apiKey, secretKey, jsonSerializer) 24 | { 25 | 26 | } 27 | 28 | /// 29 | /// 获取人头中间的X坐标 30 | /// 31 | /// 32 | /// 33 | public Task BodyAnalysis(byte[] image_bytes) 34 | { 35 | try 36 | { 37 | var image = Convert.ToBase64String(image_bytes); 38 | return DoPostForm("https://aip.baidubce.com/rest/2.0/image-classify/v1/body_analysis", new Dictionary() { ["image"] = image }); 39 | } 40 | catch { } 41 | 42 | return Task.FromResult(null); 43 | } 44 | } 45 | 46 | public class BaiduBodyAnalysisResult 47 | { 48 | public int person_num { get; set; } 49 | public BaiduBodyAnalysisPersonInfo[] person_info { get; set; } 50 | public string log_id { get; set; } 51 | } 52 | 53 | public class BaiduBodyAnalysisPersonInfo 54 | { 55 | public BaiduBodyAnalysisBodyParts body_parts { get; set; } 56 | public BaiduBodyAnalysisBodyLocation location { get; set; } 57 | } 58 | 59 | public class BaiduBodyAnalysisBodyParts 60 | { 61 | public BaiduBodyAnalysisBodyPoint left_hip { get; set; } 62 | public BaiduBodyAnalysisBodyPoint top_head { get; set; } 63 | public BaiduBodyAnalysisBodyPoint right_mouth_corner { get; set; } 64 | public BaiduBodyAnalysisBodyPoint neck { get; set; } 65 | public BaiduBodyAnalysisBodyPoint left_shoulder { get; set; } 66 | public BaiduBodyAnalysisBodyPoint left_knee { get; set; } 67 | public BaiduBodyAnalysisBodyPoint left_ankle { get; set; } 68 | public BaiduBodyAnalysisBodyPoint left_mouth_corner { get; set; } 69 | public BaiduBodyAnalysisBodyPoint right_elbow { get; set; } 70 | public BaiduBodyAnalysisBodyPoint right_ear { get; set; } 71 | public BaiduBodyAnalysisBodyPoint nose { get; set; } 72 | public BaiduBodyAnalysisBodyPoint left_eye { get; set; } 73 | public BaiduBodyAnalysisBodyPoint right_eye { get; set; } 74 | public BaiduBodyAnalysisBodyPoint right_hip { get; set; } 75 | public BaiduBodyAnalysisBodyPoint left_wrist { get; set; } 76 | public BaiduBodyAnalysisBodyPoint left_ear { get; set; } 77 | public BaiduBodyAnalysisBodyPoint left_elbow { get; set; } 78 | public BaiduBodyAnalysisBodyPoint right_shoulder { get; set; } 79 | public BaiduBodyAnalysisBodyPoint right_ankle { get; set; } 80 | public BaiduBodyAnalysisBodyPoint right_knee { get; set; } 81 | public BaiduBodyAnalysisBodyPoint right_wrist { get; set; } 82 | } 83 | 84 | public class BaiduBodyAnalysisBodyPoint 85 | { 86 | public float y { get; set; } 87 | public float x { get; set; } 88 | public float score { get; set; } 89 | } 90 | 91 | public class BaiduBodyAnalysisBodyLocation 92 | { 93 | public float height { get; set; } 94 | public float width { get; set; } 95 | public float top { get; set; } 96 | public float score { get; set; } 97 | public float left { get; set; } 98 | } 99 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Configuration/JavOrganizationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Emby.Plugins.JavScraper.Configuration 2 | { 3 | /// 4 | /// 视频文件整理配置 5 | /// 6 | public class JavOrganizationOptions 7 | { 8 | /// 9 | /// 源文件夹 10 | /// 11 | public string[] WatchLocations { get; set; } 12 | 13 | /// 14 | /// 目标位置 15 | /// 16 | public string TargetLocation { get; set; } 17 | 18 | /// 19 | /// 最小视频文件大小 20 | /// 21 | public int MinFileSizeMb { get; set; } 22 | 23 | /// 24 | /// 影片文件夹表达式 25 | /// 26 | public string MovieFolderPattern { get; set; } 27 | 28 | /// 29 | /// 影片名表达式 30 | /// 31 | public string MoviePattern { get; set; } 32 | 33 | /// 34 | /// 增加中文字幕后缀(-C),0不加,1文件夹,2,文件名,3,文件夹和文件名 35 | /// 36 | public int AddChineseSubtitleSuffix { get; set; } 37 | 38 | 39 | /// 40 | /// 复制或者移动原始文件 41 | /// 42 | public bool CopyOriginalFile { get; set; } 43 | 44 | /// 45 | /// 覆盖已存在的文件 46 | /// 47 | public bool OverwriteExistingFiles { get; set; } 48 | 49 | /// 50 | /// 删除以下扩展名的文件 51 | /// 52 | public string[] LeftOverFileExtensionsToDelete { get; set; } 53 | 54 | /// 55 | /// 删除空文件夹 56 | /// 57 | public bool DeleteEmptyFolders { get; set; } 58 | 59 | /// 60 | /// 扩展清理剩余的文件 61 | /// 62 | public bool ExtendedClean { get; set; } 63 | 64 | 65 | public JavOrganizationOptions() 66 | { 67 | MinFileSizeMb = 50; 68 | AddChineseSubtitleSuffix = 3; 69 | LeftOverFileExtensionsToDelete = new string[] { }; 70 | MovieFolderPattern = "%actor%/%num% %title_original%"; 71 | MoviePattern = "%num%"; 72 | WatchLocations = new string[] { }; 73 | CopyOriginalFile = false; 74 | DeleteEmptyFolders = true; 75 | ExtendedClean = false; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Scrapers; 2 | using LiteDB; 3 | using MediaBrowser.Common.Configuration; 4 | using System; 5 | using System.IO; 6 | 7 | namespace Emby.Plugins.JavScraper.Data 8 | { 9 | /// 10 | /// 数据库访问实体 11 | /// 12 | public class ApplicationDbContext : LiteDatabase 13 | { 14 | /// 15 | /// 影片情节信息 16 | /// 17 | public ILiteCollection Plots { get; } 18 | 19 | /// 20 | /// 元数据 21 | /// 22 | public ILiteCollection Metadata { get; } 23 | 24 | /// 25 | /// 翻译 26 | /// 27 | public ILiteCollection Translations { get; } 28 | 29 | /// 30 | /// 图片人脸中心点位置 31 | /// 32 | public ILiteCollection ImageFaceCenterPoints { get; } 33 | 34 | /// 35 | /// 构造 36 | /// 37 | /// 38 | public ApplicationDbContext(string connectionString) 39 | : base(connectionString) 40 | { 41 | Plots = GetCollection("Plots"); 42 | Metadata = GetCollection("Metadata"); 43 | Translations = GetCollection("Translations"); 44 | ImageFaceCenterPoints = GetCollection("ImageFaceCenterPoints"); 45 | 46 | Plots.EnsureIndex(o => o.num); 47 | Plots.EnsureIndex(o => o.provider); 48 | 49 | Metadata.EnsureIndex(o => o.num); 50 | Metadata.EnsureIndex(o => o.provider); 51 | Metadata.EnsureIndex(o => o.url); 52 | 53 | Translations.EnsureIndex(o => o.hash); 54 | Translations.EnsureIndex(o => o.lang); 55 | } 56 | 57 | /// 58 | /// 创建数据库实体 59 | /// 60 | /// 61 | /// 62 | public static ApplicationDbContext Create(IApplicationPaths applicationPaths) 63 | { 64 | var path = Path.Combine(applicationPaths.DataPath, "JavScraper.db"); 65 | 66 | try 67 | { 68 | return new ApplicationDbContext(path); 69 | } 70 | catch { } 71 | 72 | return default; 73 | } 74 | 75 | /// 76 | /// 保存视频元数据 77 | /// 78 | /// 79 | public bool SaveJavVideo(JavVideo video) 80 | { 81 | try 82 | { 83 | var d = Metadata.FindOne(o => o.url == video.Url && o.provider == video.Provider); 84 | var dt = DateTime.Now; 85 | if (d == null) 86 | { 87 | d = new Data.Metadata() 88 | { 89 | created = dt, 90 | data = video, 91 | modified = dt, 92 | num = video.Num, 93 | provider = video.Provider, 94 | url = video.Url, 95 | selected = dt 96 | }; 97 | Metadata.Insert(d); 98 | } 99 | else 100 | { 101 | d.modified = dt; 102 | d.selected = dt; 103 | d.num = video.Num; 104 | d.data = video; 105 | Metadata.Update(d); 106 | } 107 | } 108 | catch 109 | { 110 | } 111 | return false; 112 | } 113 | 114 | /// 115 | /// 查找视频元数据 116 | /// 117 | /// 118 | /// 119 | /// 120 | public JavVideo FindJavVideo(string provider, string url) 121 | { 122 | if (string.IsNullOrWhiteSpace(provider)) 123 | return Metadata.FindOne(o => o.url == url)?.data; 124 | else 125 | return Metadata.FindOne(o => o.url == url && o.provider == provider)?.data; 126 | } 127 | 128 | /// 129 | /// 查找视频元数据 130 | /// 131 | /// 132 | /// 133 | /// 134 | public Metadata FindMetadata(string provider, string url) 135 | { 136 | if (string.IsNullOrWhiteSpace(provider)) 137 | return Metadata.FindOne(o => o.url == url); 138 | else 139 | return Metadata.FindOne(o => o.url == url && o.provider == provider); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Data/ImageFaceCenterPoint.cs: -------------------------------------------------------------------------------- 1 | using LiteDB; 2 | using System; 3 | 4 | namespace Emby.Plugins.JavScraper.Data 5 | { 6 | /// 7 | /// 图片人脸中心点位置 8 | /// 9 | public class ImageFaceCenterPoint 10 | { 11 | /// 12 | /// url 地址 13 | /// 14 | [BsonId] 15 | public string url { get; set; } 16 | 17 | /// 18 | /// 中心点位置 19 | /// 20 | public double point { get; set; } 21 | 22 | /// 23 | /// 创建时间 24 | /// 25 | public DateTime created { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Data/Metadata.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Scrapers; 2 | using LiteDB; 3 | using System; 4 | 5 | namespace Emby.Plugins.JavScraper.Data 6 | { 7 | /// 8 | /// 影片元数据 9 | /// 10 | public class Metadata 11 | { 12 | /// 13 | /// id 14 | /// 15 | [BsonId] 16 | public ObjectId id { get; set; } 17 | 18 | /// 19 | /// 适配器 20 | /// 21 | public string provider { get; set; } 22 | 23 | /// 24 | /// 番号 25 | /// 26 | public string num { get; set; } 27 | 28 | /// 29 | /// 链接地址 30 | /// 31 | public string url { get; set; } 32 | 33 | /// 34 | /// 数据 35 | /// 36 | public JavVideo data { get; set; } 37 | 38 | /// 39 | /// 最后选中时间 40 | /// 41 | public DateTime? selected { get; set; } 42 | 43 | /// 44 | /// 修改时间 45 | /// 46 | public DateTime modified { get; set; } 47 | 48 | /// 49 | /// 创建时间 50 | /// 51 | public DateTime created { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Data/Plot.cs: -------------------------------------------------------------------------------- 1 | using LiteDB; 2 | using System; 3 | 4 | namespace Emby.Plugins.JavScraper.Data 5 | { 6 | /// 7 | /// 影片情节信息 8 | /// 9 | public class Plot 10 | { 11 | /// 12 | /// id 13 | /// 14 | [BsonId] 15 | public ObjectId id { get; set; } 16 | 17 | /// 18 | /// 适配器 19 | /// 20 | public string provider { get; set; } 21 | 22 | /// 23 | /// 去掉下划线和横线的番号 24 | /// 25 | public string num { get; set; } 26 | 27 | /// 28 | /// 链接地址 29 | /// 30 | public string url { get; set; } 31 | 32 | /// 33 | /// 简介 34 | /// 35 | public string plot { get; set; } 36 | 37 | /// 38 | /// 修改时间 39 | /// 40 | public DateTime modified { get; set; } 41 | 42 | /// 43 | /// 创建时间 44 | /// 45 | public DateTime created { get; set; } 46 | } 47 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Data/Translation.cs: -------------------------------------------------------------------------------- 1 | using LiteDB; 2 | using System; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace Emby.Plugins.JavScraper.Data 7 | { 8 | /// 9 | /// 翻译 10 | /// 11 | public class Translation 12 | { 13 | /// 14 | /// id 15 | /// 16 | [BsonId] 17 | public ObjectId id { get; set; } 18 | 19 | /// 20 | /// 原始文本的MD5结果 21 | /// 22 | public string hash { get; set; } 23 | 24 | /// 25 | /// 目标语言 26 | /// 27 | public string lang { get; set; } 28 | 29 | /// 30 | /// 原始文本 31 | /// 32 | public string src { get; set; } 33 | 34 | /// 35 | /// 翻译结果 36 | /// 37 | public string dst { get; set; } 38 | 39 | 40 | /// 41 | /// 修改时间 42 | /// 43 | public DateTime modified { get; set; } 44 | 45 | /// 46 | /// 创建时间 47 | /// 48 | public DateTime created { get; set; } 49 | 50 | /// 51 | /// 计算原始文本的 Hash 52 | /// 53 | /// 54 | /// 55 | public static string CalcHash(string src) 56 | { 57 | if (string.IsNullOrWhiteSpace(src)) 58 | return string.Empty; 59 | 60 | using (var md5 = MD5.Create()) 61 | { 62 | var result = md5.ComputeHash(Encoding.UTF8.GetBytes(src)); 63 | var strResult = BitConverter.ToString(result); 64 | return strResult.Replace("-", "").ToLower(); 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Emby.Plugins.JavScraper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | JavScraper 6 | Copyright © $([System.DateTime]::Now.Year) JavScraper 7 | 1.$([System.DateTime]::Now.ToString(yyyy.MMdd.HHmm)) 8 | https://github.com/JavScraper/Emby.Plugins.JavScraper 9 | Git 10 | https://github.com/JavScraper/Emby.Plugins.JavScraper.git 11 | thumb.png 12 | 13 | JavScraper@gmail.com 14 | Debug;Release;Debug.Jellyfin;Release.Jellyfin 15 | true 16 | 17 | 18 | 19 | __JELLYFIN__ 20 | 21 | 22 | 23 | TRACE;__JELLYFIN__ 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | True 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 | -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Extensions/ILogManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | #if !__JELLYFIN__ 2 | 3 | using System; 4 | 5 | namespace MediaBrowser.Model.Logging 6 | { 7 | /// 8 | /// ILogManager 扩展 9 | /// 10 | public static class ILogManagerExtensions 11 | { 12 | /// 13 | /// 创建日志记录器 14 | /// 15 | /// 类型 16 | /// 日志管理器 17 | /// 18 | public static ILogger CreateLogger(this ILogManager factory) 19 | => factory.GetLogger(typeof(T).FullName); 20 | 21 | /// 22 | /// 创建日志记录器 23 | /// 24 | /// 日志管理器 25 | /// 类型 26 | /// 27 | public static ILogger CreateLogger(this ILogManager factory, Type type) 28 | => factory.GetLogger(type.FullName); 29 | } 30 | } 31 | 32 | #endif -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Extensions/JellyfinExtensions.cs: -------------------------------------------------------------------------------- 1 | #if __JELLYFIN__ 2 | 3 | using MediaBrowser.Common.Configuration; 4 | using MediaBrowser.Controller.Entities; 5 | using MediaBrowser.Controller.Library; 6 | using MediaBrowser.Model.Extensions; 7 | using MediaBrowser.Model.IO; 8 | using Microsoft.Extensions.Logging; 9 | using System.IO; 10 | using System.Threading.Tasks; 11 | 12 | namespace Emby.Plugins.JavScraper 13 | { 14 | public static class JellyfinExtensions 15 | { 16 | private static string[] SubtitleExtensions = new[] { ".srt", ".ssa", ".ass", ".sub", ".smi", ".sami", ".vtt", ".mpl" }; 17 | 18 | public static bool IsSubtitleFile(this ILibraryManager _, string path) 19 | { 20 | var extension = Path.GetExtension(path); 21 | return ListHelper.ContainsIgnoreCase(SubtitleExtensions, extension); 22 | } 23 | 24 | public static void UpdateToRepository(this BaseItem item, ItemUpdateType type) 25 | => item.UpdateToRepository(type, default); 26 | 27 | /// 28 | /// 获取图片缓存路径 29 | /// 30 | /// 31 | /// 32 | public static string GetImageCachePath(this IApplicationPaths appPaths) 33 | => appPaths.ImageCachePath; 34 | 35 | public static void WriteAllBytes(this IFileSystem fs, string path, byte[] bytes) 36 | { 37 | File.WriteAllBytes(path, bytes); 38 | } 39 | 40 | public static Task ReadAllBytesAsync(this IFileSystem fs, string path) 41 | { 42 | return Task.FromResult(File.ReadAllBytes(path)); 43 | } 44 | 45 | public static bool DirectoryExists(this IFileSystem fileSystem, string path) 46 | => Directory.Exists(path); 47 | 48 | public static bool FileExists(this IFileSystem fileSystem, string path) 49 | => File.Exists(path); 50 | 51 | public static void CreateDirectory(this IFileSystem fileSystem, string path) 52 | => Directory.CreateDirectory(path); 53 | 54 | public static void CopyFile(this IFileSystem fileSystem, string source, string target, bool overwrite) 55 | => File.Copy(source, target, overwrite); 56 | 57 | public static void MoveFile(this IFileSystem fileSystem, string source, string target) 58 | => File.Move(source, target); 59 | 60 | public static void MoveDirectory(this IFileSystem fileSystem, string source, string target) 61 | => Directory.Move(source, target); 62 | 63 | public static void DeleteDirectory(this IFileSystem fileSystem, string path, bool recursive) 64 | => Directory.Delete(path, recursive); 65 | 66 | public static void Debug(this ILogger logger, string msg) 67 | => logger.LogDebug(msg); 68 | 69 | public static void Info(this ILogger logger, string msg) 70 | => logger.LogInformation(msg); 71 | 72 | public static void Warn(this ILogger logger, string msg) 73 | => logger.LogWarning(msg); 74 | 75 | public static void Error(this ILogger logger, string msg) 76 | => logger.LogError(msg); 77 | } 78 | } 79 | 80 | #endif -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Extensions/NamedLockerAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.Plugins.JavScraper 7 | { 8 | /// 9 | /// 命名锁 10 | /// 11 | public class NamedLockerAsync 12 | { 13 | private readonly ConcurrentDictionary _lockDict = new ConcurrentDictionary(); 14 | 15 | /// 16 | /// 获取锁对象 17 | /// 18 | /// 19 | /// 20 | public SemaphoreSlim GetLock(string name) 21 | => _lockDict.GetOrAdd(name, s => new SemaphoreSlim(1, 1)); 22 | 23 | /// 24 | /// 执行带返回结果的锁定 25 | /// 26 | /// 返回结果 27 | /// 锁名 28 | /// 执行方法 29 | /// 自动移除锁对象 30 | /// 31 | public async Task RunWithLock(string name, Func> func, bool auto_remove = true) 32 | { 33 | var locker = _lockDict.GetOrAdd(name, s => new SemaphoreSlim(1, 1)); 34 | await locker.WaitAsync(); 35 | try 36 | { 37 | var t = await func(); 38 | return t; 39 | } 40 | finally 41 | { 42 | locker.Release(); 43 | Thread.Sleep(1); 44 | if (locker.CurrentCount == 1) 45 | { 46 | _lockDict.TryRemove(name, out locker); 47 | if (locker.CurrentCount == 1) 48 | locker.Dispose(); 49 | } 50 | } 51 | } 52 | 53 | /// 54 | /// 执行带返回结果的锁定 55 | /// 56 | /// 锁名 57 | /// The action. 58 | /// 自动移除锁对象 59 | /// 60 | public async Task RunWithLock(string name, Func action, bool auto_remove = true) 61 | { 62 | var locker = _lockDict.GetOrAdd(name, s => new SemaphoreSlim(1, 1)); 63 | await locker.WaitAsync(); 64 | try 65 | { 66 | await action(); 67 | } 68 | finally 69 | { 70 | locker.Release(); 71 | Thread.Sleep(1); 72 | if (locker.CurrentCount == 1) 73 | { 74 | _lockDict.TryRemove(name, out locker); 75 | if (locker.CurrentCount == 1) 76 | locker.Dispose(); 77 | } 78 | } 79 | } 80 | 81 | public void RemoveLock(string name) 82 | { 83 | SemaphoreSlim o; 84 | _lockDict.TryRemove(name, out o); 85 | } 86 | 87 | public Task LockAsync(string name) 88 | { 89 | var m_semaphore = GetLock(name); 90 | var wait = m_semaphore.WaitAsync(); 91 | var m_releaser = Task.FromResult((IDisposable)new Releaser(this, name, m_semaphore)); 92 | return wait.IsCompleted ? 93 | m_releaser : 94 | wait.ContinueWith((_, state) => (IDisposable)state, 95 | m_releaser.Result, CancellationToken.None, 96 | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 97 | } 98 | 99 | private sealed class Releaser : IDisposable 100 | { 101 | private readonly NamedLockerAsync named_locker; 102 | private readonly string name; 103 | private readonly SemaphoreSlim m_semaphore; 104 | 105 | internal Releaser(NamedLockerAsync named_locker, string name, SemaphoreSlim m_semaphore) 106 | { 107 | this.named_locker = named_locker; 108 | this.name = name; 109 | this.m_semaphore = m_semaphore; 110 | } 111 | 112 | public void Dispose() 113 | { 114 | m_semaphore.Release(); 115 | Thread.Sleep(1); 116 | if (m_semaphore.CurrentCount == 1) 117 | { 118 | named_locker.RemoveLock(name); 119 | if (m_semaphore.CurrentCount == 1) 120 | m_semaphore.Dispose(); 121 | } 122 | } 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Extensions/PluginExtensions.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Scrapers; 2 | using MediaBrowser.Model.Entities; 3 | using MediaBrowser.Model.Serialization; 4 | 5 | namespace Emby.Plugins.JavScraper 6 | { 7 | /// 8 | /// 扩展 9 | /// 10 | public static class PluginExtensions 11 | { 12 | public static string Name => Plugin.NAME; 13 | 14 | public static string PersonName => Plugin.NAME + "-Actress"; 15 | 16 | /// 17 | /// 设置视频信息 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static IHasProviderIds SetJavVideoIndex(this IHasProviderIds result, IJsonSerializer _jsonSerializer, JavVideoIndex m) 24 | { 25 | result.ProviderIds[Name] = m.Num; 26 | result.ProviderIds[$"{Name}-Json"] = _jsonSerializer.SerializeToString(m); 27 | result.ProviderIds[$"{Name}-Url"] = m.Url; 28 | 29 | return result; 30 | } 31 | 32 | /// 33 | /// 获取视频信息 34 | /// 35 | /// 36 | /// 37 | /// 38 | public static JavVideo GetJavVideoIndex(this IHasProviderIds result, IJsonSerializer _jsonSerializer) 39 | { 40 | if (result.ProviderIds.TryGetValue($"{Name}-Json", out string json) == false) 41 | return null; 42 | 43 | return _jsonSerializer.DeserializeFromString(json); 44 | } 45 | 46 | /// 47 | /// 设置头像信息 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | public static IHasProviderIds SetJavPersonIndex(this IHasProviderIds result, IJsonSerializer _jsonSerializer, JavPersonIndex m) 54 | { 55 | result.ProviderIds[PersonName] = m.Url; 56 | result.ProviderIds[$"{PersonName}-Json"] = _jsonSerializer.SerializeToString(m); 57 | result.ProviderIds[$"{PersonName}-Url"] = m.Url; 58 | 59 | return result; 60 | } 61 | 62 | /// 63 | /// 获取头像信息 64 | /// 65 | /// 66 | /// 67 | /// 68 | public static JavPersonIndex GetJavPersonIndex(this IHasProviderIds result, IJsonSerializer _jsonSerializer) 69 | { 70 | if (result.ProviderIds.TryGetValue($"{PersonName}-Json", out string json) == false) 71 | return null; 72 | 73 | return _jsonSerializer.DeserializeFromString(json); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Emby.Plugins.JavScraper 7 | { 8 | public static class StringExtensions 9 | { 10 | private static readonly Regex WebUrlExpression = new Regex(@"(http|https)://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?", RegexOptions.Singleline | RegexOptions.Compiled); 11 | 12 | [DebuggerStepThrough] 13 | public static bool IsWebUrl(this string target) 14 | { 15 | return !string.IsNullOrEmpty(target) && WebUrlExpression.IsMatch(target); 16 | } 17 | 18 | public static string TrimStart(this string target, string trimString) 19 | { 20 | if (string.IsNullOrEmpty(trimString)) 21 | return target; 22 | 23 | string result = target; 24 | while (result.StartsWith(trimString, StringComparison.OrdinalIgnoreCase)) 25 | { 26 | result = result.Substring(trimString.Length); 27 | } 28 | 29 | return result; 30 | } 31 | 32 | public static string TrimEnd(this string target, params string[] trimStrings) 33 | { 34 | trimStrings = trimStrings?.Where(o => string.IsNullOrEmpty(o) == false).Distinct().ToArray(); 35 | if (trimStrings?.Any() != true) 36 | return target; 37 | 38 | var found = false; 39 | 40 | do 41 | { 42 | found = false; 43 | foreach (var trimString in trimStrings) 44 | { 45 | while (target.EndsWith(trimString, StringComparison.OrdinalIgnoreCase)) 46 | { 47 | target = target.Substring(0, target.Length - trimString.Length); 48 | found = true; 49 | } 50 | } 51 | } while (found); 52 | return target; 53 | } 54 | 55 | public static string Trim(this string target, string trimString) 56 | => target.TrimStart(trimString).TrimEnd(trimString); 57 | } 58 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/FixChineseSubtitleGenreTask.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Controller.Library; 2 | using MediaBrowser.Controller.Providers; 3 | using MediaBrowser.Model.IO; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using MediaBrowser.Model.Tasks; 8 | using System.Collections.Generic; 9 | using MediaBrowser.Model.Serialization; 10 | using MediaBrowser.Common.Configuration; 11 | using System.Linq; 12 | using System.IO; 13 | using MediaBrowser.Controller.Entities.Movies; 14 | using Emby.Plugins.JavScraper.Configuration; 15 | 16 | #if __JELLYFIN__ 17 | using Microsoft.Extensions.Logging; 18 | #else 19 | using MediaBrowser.Model.Logging; 20 | #endif 21 | 22 | namespace Emby.Plugins.JavScraper 23 | { 24 | public class FixChineseSubtitleGenreTask : IScheduledTask 25 | { 26 | public string Name => Plugin.NAME + ": 修复缺失的中文字幕标签"; 27 | public string Key => Plugin.NAME + "-FixChineseSubtitleGenre"; 28 | public string Description => "修复缺失的中文字幕标签,需要在配置中勾选【给 -C 或 -C2 结尾的影片增加“中文字幕”标签】选项。"; 29 | public string Category => "JavScraper"; 30 | 31 | private readonly ILibraryManager _libraryManager; 32 | private readonly IJsonSerializer _jsonSerializer; 33 | private readonly IApplicationPaths appPaths; 34 | private readonly IProviderManager providerManager; 35 | private readonly ILibraryMonitor libraryMonitor; 36 | private readonly IFileSystem _fileSystem; 37 | private readonly ILogger _logger; 38 | 39 | public FixChineseSubtitleGenreTask( 40 | #if __JELLYFIN__ 41 | ILoggerFactory logManager 42 | #else 43 | ILogManager logManager 44 | #endif 45 | , ILibraryManager libraryManager, IJsonSerializer _jsonSerializer, IApplicationPaths appPaths, 46 | IProviderManager providerManager, 47 | ILibraryMonitor libraryMonitor, 48 | IFileSystem fileSystem) 49 | { 50 | _logger = logManager.CreateLogger(); 51 | this._libraryManager = libraryManager; 52 | this._jsonSerializer = _jsonSerializer; 53 | this.appPaths = appPaths; 54 | this.providerManager = providerManager; 55 | this.libraryMonitor = libraryMonitor; 56 | this._fileSystem = fileSystem; 57 | } 58 | 59 | public IEnumerable GetDefaultTriggers() 60 | => new TaskTriggerInfo[] { }; 61 | 62 | public async Task Execute(CancellationToken cancellationToken, IProgress progress) 63 | { 64 | await Task.Yield(); 65 | if (Plugin.Instance.Configuration.AddChineseSubtitleGenre == false) 66 | { 67 | _logger.Warn($"AddChineseSubtitleGenre option is not enabled."); 68 | return; 69 | } 70 | _logger.Info($"Running..."); 71 | progress.Report(0); 72 | 73 | var libraryFolderPaths = _libraryManager.GetVirtualFolders() 74 | .Where(dir => dir.CollectionType == "movies" && dir.Locations?.Any() == true && 75 | dir.LibraryOptions.TypeOptions?.Any(o => o.MetadataFetchers?.Contains(Plugin.NAME) == true) == true) 76 | .SelectMany(o => o.Locations).ToList(); 77 | 78 | var eligibleFiles = libraryFolderPaths.SelectMany(GetVideoFiles) 79 | .OrderBy(_fileSystem.GetCreationTimeUtc) 80 | .Where(i => IsVideoFile(i)) 81 | .ToList(); 82 | 83 | _logger.Info($"{eligibleFiles.Count} files found"); 84 | if (eligibleFiles.Count == 0) 85 | { 86 | progress.Report(100); 87 | return; 88 | } 89 | 90 | int index = 0; 91 | foreach (var m in eligibleFiles) 92 | { 93 | try 94 | { 95 | var r = Do(m); 96 | } 97 | catch (Exception ex) 98 | { 99 | _logger.Error($"{m.FullName} {ex.Message}"); 100 | } 101 | index++; 102 | progress.Report(index * 1.0 / eligibleFiles.Count * 100); 103 | } 104 | progress.Report(100); 105 | } 106 | 107 | private bool Do(FileSystemMetadata m) 108 | { 109 | var movie = _libraryManager.FindByPath(m.FullName, false) as Movie; 110 | if (movie == null) 111 | { 112 | _logger.Error($"the movie does not exists. {m.FullName}"); 113 | return false; 114 | } 115 | 116 | const string CHINESE_SUBTITLE_GENRE = "中文字幕"; 117 | 118 | if (movie.Genres?.Contains(CHINESE_SUBTITLE_GENRE) == true) 119 | return true; 120 | 121 | var arr = new[] { Path.GetFileNameWithoutExtension(m.FullName), Path.GetFileName(Path.GetDirectoryName(m.FullName)) }; 122 | var cc = new[] { "-C", "-C2", "_C", "_C2" }; 123 | var has_chinese_subtitle = arr.Any(v => cc.Any(x => v.EndsWith(x, StringComparison.OrdinalIgnoreCase))) 124 | || ExistsSubtitleFile(m); 125 | 126 | if (has_chinese_subtitle == false) 127 | return false; 128 | 129 | movie.AddGenre(CHINESE_SUBTITLE_GENRE); 130 | movie.UpdateToRepository(ItemUpdateType.MetadataEdit); 131 | return true; 132 | } 133 | 134 | /// 135 | /// 是否存在字幕文件 136 | /// 137 | /// 138 | /// 139 | private bool ExistsSubtitleFile(FileSystemMetadata fileInfo) 140 | { 141 | try 142 | { 143 | var name = Path.GetFileNameWithoutExtension(fileInfo.FullName); 144 | var files = _fileSystem.GetFilePaths(Path.GetDirectoryName(fileInfo.FullName)); 145 | return files.Any(v => v.StartsWith(name, StringComparison.OrdinalIgnoreCase) && _libraryManager.IsSubtitleFile(v 146 | #if !__JELLYFIN__ 147 | .AsSpan() 148 | #endif 149 | )); 150 | } 151 | catch (Exception ex) 152 | { 153 | _logger.Error($"Error organizing file {fileInfo.Name}: {ex.Message}"); 154 | } 155 | 156 | return false; 157 | } 158 | 159 | /// 160 | /// Gets the files to organize. 161 | /// 162 | /// The path. 163 | /// IEnumerable{FileInfo}. 164 | private List GetVideoFiles(string path) 165 | { 166 | try 167 | { 168 | return _fileSystem.GetFiles(path, true).Where(file => IsVideoFile(file)).ToList(); 169 | } 170 | catch (DirectoryNotFoundException) 171 | { 172 | _logger.Info($"folder does not exist: {path}"); 173 | 174 | return new List(); 175 | } 176 | catch (IOException ex) 177 | { 178 | _logger.Error($"Error getting files from {path}: {ex.Message}"); 179 | 180 | return new List(); 181 | } 182 | } 183 | 184 | /// 185 | /// 是否是视频文件 186 | /// 187 | /// 188 | /// 189 | private bool IsVideoFile(FileSystemMetadata fileInfo) 190 | { 191 | try 192 | { 193 | return _libraryManager.IsVideoFile(fileInfo.FullName 194 | #if !__JELLYFIN__ 195 | .AsSpan() 196 | #endif 197 | ); 198 | } 199 | catch (Exception ex) 200 | { 201 | _logger.Error($"Error organizing file {fileInfo.Name}: {ex.Message}"); 202 | } 203 | 204 | return false; 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Http/HttpClientEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Emby.Plugins.JavScraper.Http 9 | { 10 | /// 11 | /// HttpClient 12 | /// 13 | public class HttpClientEx 14 | { 15 | /// 16 | /// 客户端初始话方法 17 | /// 18 | private readonly Action ac; 19 | 20 | /// 21 | /// 当前客户端 22 | /// 23 | private HttpClient client = null; 24 | 25 | /// 26 | /// 配置版本号 27 | /// 28 | private long version = -1; 29 | 30 | /// 31 | /// 上一个客户端 32 | /// 33 | private HttpClient client_old = null; 34 | 35 | public HttpClientEx(Action ac = null) 36 | { 37 | this.ac = ac; 38 | } 39 | 40 | /// 41 | /// 获取一个 HttpClient 42 | /// 43 | /// 44 | [MethodImpl(MethodImplOptions.Synchronized)] 45 | public HttpClient GetClient() 46 | { 47 | if (client != null && version == Plugin.Instance.Configuration.ConfigurationVersion) 48 | return client; 49 | 50 | if (client_old != null) 51 | { 52 | client_old.Dispose(); 53 | client_old = null; 54 | } 55 | client_old = client; 56 | 57 | var handler = new ProxyHttpClientHandler(); 58 | client = new HttpClient(handler, true); 59 | ac?.Invoke(client); 60 | 61 | return client; 62 | } 63 | 64 | public Task GetStringAsync(string requestUri) 65 | => GetClient().GetStringAsync(requestUri); 66 | 67 | public Task GetAsync(string requestUri) 68 | => GetClient().GetAsync(requestUri); 69 | 70 | public Task GetAsync(string requestUri, CancellationToken cancellationToken) 71 | => GetClient().GetAsync(requestUri, cancellationToken); 72 | 73 | public Task GetStreamAsync(string requestUri) 74 | => GetClient().GetStreamAsync(requestUri); 75 | 76 | public Task PostAsync(string requestUri, HttpContent content) 77 | => GetClient().PostAsync(requestUri, content); 78 | 79 | public Uri BaseAddress => GetClient().BaseAddress; 80 | } 81 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Http/JavWebProxy.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Configuration; 2 | using MihaZupan; 3 | using System; 4 | using System.Net; 5 | 6 | namespace Emby.Plugins.JavScraper.Http 7 | { 8 | /// 9 | /// 代理服务器 10 | /// 11 | public class JavWebProxy : IWebProxy 12 | { 13 | private IWebProxy proxy; 14 | 15 | /// 16 | /// 代理服务 17 | /// 18 | public IWebProxy Proxy 19 | { 20 | get => proxy ?? WebRequest.DefaultWebProxy; 21 | set => proxy = value; 22 | } 23 | 24 | /// 25 | /// The credentials to submit to the proxy server for authentication. 26 | /// 27 | public ICredentials Credentials 28 | { 29 | get => Proxy.Credentials; 30 | set => Proxy.Credentials = value; 31 | } 32 | 33 | public JavWebProxy() 34 | { 35 | Reset(); 36 | } 37 | 38 | /// 39 | /// 重设代理 40 | /// 41 | public void Reset() 42 | { 43 | var old = Proxy; 44 | var options = Plugin.Instance.Configuration; 45 | switch ((ProxyTypeEnum)options.ProxyType) 46 | { 47 | case ProxyTypeEnum.None: 48 | case ProxyTypeEnum.JsProxy: 49 | default: 50 | Proxy = null; 51 | break; 52 | 53 | case ProxyTypeEnum.HTTP: 54 | case ProxyTypeEnum.HTTPS: 55 | case ProxyTypeEnum.Socks5: 56 | { 57 | IWebProxy p = null; 58 | if (string.IsNullOrWhiteSpace(options?.ProxyHost) == false && options?.ProxyPort > 0 && options?.ProxyPort < 65535) 59 | { 60 | var hasCredential = string.IsNullOrWhiteSpace(options.ProxyUserName) == false && string.IsNullOrWhiteSpace(options.ProxyPassword) == false; 61 | if (options.ProxyType == (int)ProxyTypeEnum.HTTP || options.ProxyType == (int)ProxyTypeEnum.HTTPS) 62 | { 63 | var sm = options.ProxyType == (int)ProxyTypeEnum.HTTP ? "http" : "https"; 64 | var url = $"{sm}://{options.ProxyHost}:{options.ProxyPort}"; 65 | p = hasCredential == false ? new WebProxy(url, true) : 66 | new WebProxy(url, true, new string[] { }, new NetworkCredential() { UserName = options.ProxyUserName, Password = options.ProxyPassword }); 67 | } 68 | else 69 | p = hasCredential == false ? new HttpToSocks5Proxy(options.ProxyHost, options.ProxyPort) : 70 | new HttpToSocks5Proxy(options.ProxyHost, options.ProxyPort, options.ProxyUserName, options.ProxyPassword); 71 | } 72 | Proxy = p; 73 | break; 74 | } 75 | } 76 | if (old is HttpToSocks5Proxy s5) 77 | s5.StopInternalServer(); 78 | } 79 | 80 | /// 81 | /// Returns the URI of a proxy. 82 | /// 83 | /// A System.Uri that specifies the requested Internet resource. 84 | /// A System.Uri instance that contains the URI of the proxy used to contact destination. 85 | public Uri GetProxy(Uri destination) 86 | => Proxy.GetProxy(destination); 87 | 88 | /// 89 | /// Indicates that the proxy should not be used for the specified host. 90 | /// 91 | /// The System.Uri of the host to check for proxy use. 92 | /// 93 | public bool IsBypassed(Uri host) 94 | { 95 | var options = Plugin.Instance.Configuration; 96 | if (options.ProxyType == (int)ProxyTypeEnum.None || options.EnableJsProxy) 97 | return true; 98 | if (options.IsBypassed(host.Host)) 99 | return true; 100 | 101 | return proxy.IsBypassed(host); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Http/ProxyHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Emby.Plugins.JavScraper.Http 9 | { 10 | /// 11 | /// Proxy 客户端 12 | /// 13 | public class ProxyHttpClientHandler : HttpClientHandler 14 | { 15 | public ProxyHttpClientHandler() 16 | { 17 | //忽略SSL证书问题 18 | ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true; 19 | Proxy = new JavWebProxy(); 20 | UseProxy = true; 21 | } 22 | 23 | /// 24 | /// 发送请求 25 | /// 26 | /// 请求信息 27 | /// 取消令牌 28 | /// 29 | protected override Task SendAsync( 30 | HttpRequestMessage request, CancellationToken cancellationToken) 31 | { 32 | var cfg = Plugin.Instance.Configuration; 33 | 34 | request.Headers.Remove("X-FORWARDED-FOR"); 35 | if (cfg.EnableX_FORWARDED_FOR && !string.IsNullOrWhiteSpace(cfg.X_FORWARDED_FOR) && 36 | IPAddress.TryParse(cfg.X_FORWARDED_FOR, out var _)) 37 | request.Headers.TryAddWithoutValidation("X-FORWARDED-FOR", cfg.X_FORWARDED_FOR); 38 | 39 | //mgstage.com 加入年龄认证Cookies 40 | if (request.RequestUri.ToString().Contains("mgstage.com") && !(request.Headers.TryGetValues("Cookie", out var cookies) && cookies.Contains("abc=1"))) 41 | request.Headers.Add("Cookie", "adc=1"); 42 | 43 | //dmm.co.jp 加入年龄认证Cookies 44 | if (request.RequestUri.ToString().Contains("dmm.co.jp") && !(request.Headers.TryGetValues("Cookie", out var cookies2) && cookies2.Contains("age_check_done=1"))) 45 | request.Headers.Add("Cookie", "age_check_done=1"); 46 | 47 | // Add UserAgent 48 | if (!(request.Headers.UserAgent?.Count() > 0)) 49 | request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"); 50 | 51 | if (cfg.EnableJsProxy == false) 52 | { 53 | if (request.Headers.Referrer == null) 54 | request.Headers.Referrer = request.RequestUri; 55 | 56 | return base.SendAsync(request, cancellationToken); 57 | } 58 | 59 | var jsproxy_url = cfg.JsProxy; 60 | // Add header to request here 61 | var url = request.RequestUri.ToString(); 62 | var org_url = url; 63 | var i = org_url.IndexOf("/http/", StringComparison.CurrentCultureIgnoreCase); 64 | if (i > 0) 65 | org_url = org_url.Substring(i + 6); 66 | 67 | var uri_org = new Uri(org_url); 68 | var bypass = cfg.IsBypassed(uri_org.Host); 69 | 70 | if (bypass) 71 | { 72 | if (url != org_url) 73 | request.RequestUri = new Uri(org_url); 74 | } 75 | else if (url.StartsWith(jsproxy_url, StringComparison.OrdinalIgnoreCase) != true) 76 | { 77 | url = $"{cfg.JsProxy.TrimEnd("/")}/http/{url}"; 78 | request.RequestUri = new Uri(url); 79 | } 80 | 81 | url = request.Headers.Referrer?.ToString(); 82 | if (string.IsNullOrWhiteSpace(url)) 83 | request.Headers.Referrer = uri_org; 84 | 85 | return base.SendAsync(request, cancellationToken); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/JavIdRecognizer.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Extensions; 2 | using System; 3 | using System.IO; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Emby.Plugins.JavScraper 7 | { 8 | /// 9 | /// 番号识别 10 | /// 11 | public static class JavIdRecognizer 12 | { 13 | private static RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.Compiled; 14 | 15 | private static Func[] funcs = new Func[] { 16 | Carib,Heyzo, 17 | FC2,Musume, 18 | OnlyNumber 19 | }; 20 | 21 | /// 22 | /// 移除视频编码 1080p,720p 2k 之类的 23 | /// 24 | private static Regex p1080p = new Regex(@"(^|[^\d])(?

[\d]{3,5}p|[\d]{1,2}k)($|[^a-z])", options); 25 | 26 | public static JavId Parse(string name) 27 | { 28 | name = name.Replace("_", "-").Replace(" ", "-").Replace(".", "-"); 29 | 30 | var m = p1080p.Match(name); 31 | while (m.Success) 32 | { 33 | name = name.Replace(m.Groups["p"].Value, ""); 34 | m = m.NextMatch(); 35 | } 36 | 37 | foreach (var func in funcs) 38 | { 39 | var r = func(name); 40 | if (r != null) 41 | return r; 42 | } 43 | 44 | name = Regex.Replace(name, @"ts6[\d]+", "", options); 45 | name = Regex.Replace(name, @"-*whole\d*", "", options); 46 | name = Regex.Replace(name, @"-*full$", "", options); 47 | name = name.Replace("tokyo-hot", "", StringComparison.OrdinalIgnoreCase); 48 | name = name.TrimEnd("-C").TrimEnd("-HD", "-full", "full").TrimStart("HD-").TrimStart("h-"); 49 | name = Regex.Replace(name, @"\d{2,4}-\d{1,2}-\d{1,2}", "", options); //日期 50 | name = Regex.Replace(name, @"(.*)(00)(\d{3})", "$1-$3", options); //FANZA cid AAA00111 51 | //标准 AAA-111 52 | m = Regex.Match(name, @"(^|[^a-z0-9])(?[a-z0-9]{2,10}-[\d]{2,8})($|[^\d])", options); 53 | if (m.Success && m.Groups["id"].Value.Length >= 4) 54 | return m.Groups["id"].Value; 55 | //第二段带字母 AAA-B11 56 | m = Regex.Match(name, @"(^|[^a-z0-9])(?[a-z]{2,10}-[a-z]{1,5}[\d]{2,8})($|[^\d])", options); 57 | if (m.Success && m.Groups["id"].Value.Length >= 4) 58 | return m.Groups["id"].Value; 59 | //没有横杠的 AAA111 60 | m = Regex.Match(name, @"(^|[^a-z0-9])(?[a-z]{1,10}[\d]{2,8})($|[^\d])", options); 61 | if (m.Success && m.Groups["id"].Value.Length >= 4) 62 | return m.Groups["id"].Value; 63 | 64 | return null; 65 | } 66 | 67 | private static Regex[] regexMusume = new Regex[] { 68 | new Regex(@"(?[\d]{4,8}-[\d]{1,6})-(10mu)",options), 69 | new Regex(@"(10Musume)-(?[\d]{4,8}-[\d]{1,6})",options) 70 | }; 71 | 72 | private static JavId Musume(string name) 73 | { 74 | foreach (var regex in regexMusume) 75 | { 76 | var m = regex.Match(name); 77 | if (m.Success) 78 | return new JavId() 79 | { 80 | matcher = nameof(Musume), 81 | type = JavIdType.suren, 82 | id = m.Groups["id"].Value.Replace("_", "-") 83 | }; 84 | } 85 | return null; 86 | } 87 | 88 | private static Regex[] regexCarib = new Regex[] { 89 | new Regex(@"(?[\d]{4,8}-[\d]{1,6})-(1pon|carib|paco|mura)",options), 90 | new Regex(@"(1Pondo|Caribbean|Pacopacomama|muramura)-(?[\d]{4,8}-[\d]{1,8})($|[^\d])",options) 91 | }; 92 | 93 | private static JavId Carib(string name) 94 | { 95 | foreach (var regex in regexCarib) 96 | { 97 | var m = regex.Match(name); 98 | if (m.Success) 99 | return new JavId() 100 | { 101 | matcher = nameof(Carib), 102 | type = JavIdType.uncensored, 103 | id = m.Groups["id"].Value.Replace("-", "_") 104 | }; 105 | } 106 | return null; 107 | } 108 | 109 | private static Regex regexHeyzo = new Regex(@"Heyzo(|-| |.com)(HD-|)(?[\d]{2,8})($|[^\d])", options); 110 | 111 | private static JavId Heyzo(string name) 112 | { 113 | var m = regexHeyzo.Match(name); 114 | if (m.Success == false) 115 | return null; 116 | var id = $"heyzo-{m.Groups["id"]}"; 117 | return new JavId() 118 | { 119 | matcher = nameof(Heyzo), 120 | id = id, 121 | type = JavIdType.uncensored 122 | }; 123 | } 124 | 125 | private static Regex regexFC2 = new Regex(@"FC2-*(PPV|)[^\d]{1,3}(?[\d]{2,10})($|[^\d])", options); 126 | 127 | public static JavId FC2(string name) 128 | { 129 | var m = regexFC2.Match(name); 130 | if (m.Success == false) 131 | return null; 132 | var id = $"fc2-ppv-{m.Groups["id"]}"; 133 | return new JavId() 134 | { 135 | id = id, 136 | matcher = nameof(FC2), 137 | type = JavIdType.suren 138 | }; 139 | } 140 | 141 | private static Regex regexNumber = new Regex(@"(?[\d]{6,8}-[\d]{1,6})", options); 142 | 143 | private static JavId OnlyNumber(string name) 144 | { 145 | var m = regexNumber.Match(name); 146 | if (m.Success == false) 147 | return null; 148 | var id = m.Groups["id"].Value; 149 | return new JavId() 150 | { 151 | matcher = nameof(OnlyNumber), 152 | id = id 153 | }; 154 | } 155 | } 156 | 157 | ///

158 | /// 番号 159 | /// 160 | public class JavId 161 | { 162 | /// 163 | /// 类型 164 | /// 165 | public JavIdType type { get; set; } 166 | 167 | /// 168 | /// 解析到的id 169 | /// 170 | public string id { get; set; } 171 | 172 | /// 173 | /// 文件名 174 | /// 175 | public string file { get; set; } 176 | 177 | /// 178 | /// 匹配器 179 | /// 180 | public string matcher { get; set; } 181 | 182 | /// 183 | /// 转换为字符串 184 | /// 185 | /// 186 | public override string ToString() 187 | => id; 188 | 189 | /// 190 | /// 转换 191 | /// 192 | /// 193 | public static implicit operator JavId(string id) 194 | => new JavId() { id = id }; 195 | 196 | /// 197 | /// 转换 198 | /// 199 | /// 200 | public static implicit operator string(JavId id) 201 | => id?.id; 202 | 203 | /// 204 | /// 识别 205 | /// 206 | /// 文件路径 207 | /// 208 | public static JavId Parse(string file) 209 | { 210 | var name = Path.GetFileNameWithoutExtension(file); 211 | var id = JavIdRecognizer.Parse(name); 212 | if (id != null) 213 | id.file = file; 214 | return id; 215 | } 216 | } 217 | 218 | /// 219 | /// 类型 220 | /// 221 | public enum JavIdType 222 | { 223 | /// 224 | /// 不确定 225 | /// 226 | none, 227 | 228 | censored, 229 | uncensored, 230 | suren 231 | } 232 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/JavImageProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Emby.Plugins.JavScraper.Scrapers; 6 | using Emby.Plugins.JavScraper.Services; 7 | using MediaBrowser.Common.Configuration; 8 | using MediaBrowser.Common.Net; 9 | using MediaBrowser.Controller.Entities; 10 | using MediaBrowser.Controller.Entities.Movies; 11 | using MediaBrowser.Controller.Library; 12 | using MediaBrowser.Controller.Providers; 13 | using MediaBrowser.Model.Configuration; 14 | using MediaBrowser.Model.Entities; 15 | using MediaBrowser.Model.Providers; 16 | 17 | #if __JELLYFIN__ 18 | using Microsoft.Extensions.Logging; 19 | #else 20 | using MediaBrowser.Model.Logging; 21 | #endif 22 | 23 | using MediaBrowser.Model.Serialization; 24 | 25 | namespace Emby.Plugins.JavScraper 26 | { 27 | public class JavImageProvider : IRemoteImageProvider, IHasOrder 28 | { 29 | private readonly IProviderManager providerManager; 30 | private readonly ILibraryManager libraryManager; 31 | private readonly ImageProxyService imageProxyService; 32 | private readonly ILogger _logger; 33 | private readonly IJsonSerializer _jsonSerializer; 34 | private readonly IApplicationPaths _appPaths; 35 | public Gfriends Gfriends { get; } 36 | 37 | public int Order => 3; 38 | 39 | public JavImageProvider(IProviderManager providerManager, ILibraryManager libraryManager, 40 | #if __JELLYFIN__ 41 | ILoggerFactory logManager, 42 | #else 43 | ILogManager logManager, 44 | ImageProxyService imageProxyService, 45 | Gfriends gfriends, 46 | #endif 47 | IJsonSerializer jsonSerializer, IApplicationPaths appPaths) 48 | { 49 | this.providerManager = providerManager; 50 | this.libraryManager = libraryManager; 51 | #if __JELLYFIN__ 52 | imageProxyService = Plugin.Instance.ImageProxyService; 53 | Gfriends = new Gfriends(logManager, _jsonSerializer); 54 | #else 55 | this.imageProxyService = imageProxyService; 56 | Gfriends = gfriends; 57 | #endif 58 | _logger = logManager.CreateLogger(); 59 | _appPaths = appPaths; 60 | _jsonSerializer = jsonSerializer; 61 | } 62 | 63 | public string Name => Plugin.NAME; 64 | 65 | public Task GetImageResponse(string url, CancellationToken cancellationToken) 66 | => imageProxyService.GetImageResponse(url, ImageType.Backdrop, cancellationToken); 67 | 68 | public async Task> GetImages(BaseItem item, 69 | #if !__JELLYFIN__ 70 | LibraryOptions libraryOptions, 71 | #endif 72 | CancellationToken cancellationToken) 73 | { 74 | var list = new List(); 75 | 76 | async Task Add(string url, ImageType type) 77 | { 78 | //http://127.0.0.1:{serverApplicationHost.HttpPort} 79 | var img = new RemoteImageInfo() 80 | { 81 | Type = type, 82 | ProviderName = Name, 83 | Url = await imageProxyService.GetLocalUrl(url, type) 84 | }; 85 | list.Add(img); 86 | return img; 87 | } 88 | 89 | if (item is Movie) 90 | { 91 | JavVideoIndex index = null; 92 | if ((index = item.GetJavVideoIndex(_jsonSerializer)) == null) 93 | { 94 | _logger?.Info($"{nameof(GetImages)} name:{item.Name} JavVideoIndex not found."); 95 | return list; 96 | } 97 | 98 | var metadata = Plugin.Instance.db.FindMetadata(index.Provider, index.Url); 99 | if (metadata == null) 100 | return list; 101 | 102 | var m = metadata?.data; 103 | 104 | if (string.IsNullOrWhiteSpace(m.Cover) && m.Samples?.Any() == true) 105 | m.Cover = m.Samples.FirstOrDefault(); 106 | 107 | if (m.Cover.IsWebUrl()) 108 | { 109 | await Add(m.Cover, ImageType.Primary); 110 | await Add(m.Cover, ImageType.Backdrop); 111 | } 112 | 113 | if (m.Samples?.Any() == true) 114 | { 115 | foreach (var url in m.Samples.Where(o => o.IsWebUrl())) 116 | await Add(url, ImageType.Thumb); 117 | } 118 | } 119 | else if (item is Person) 120 | { 121 | _logger?.Info($"{nameof(GetImages)} name:{item.Name}."); 122 | 123 | JavPersonIndex index = null; 124 | if ((index = item.GetJavPersonIndex(_jsonSerializer)) == null) 125 | { 126 | var cover = await Gfriends.FindAsync(item.Name, cancellationToken); 127 | _logger?.Info($"{nameof(GetImages)} name:{item.Name} Gfriends: {cover}."); 128 | 129 | if (cover.IsWebUrl() != true) 130 | return list; 131 | 132 | index = new JavPersonIndex() { Cover = cover }; 133 | } 134 | 135 | if (!index.Cover.IsWebUrl()) 136 | { 137 | index.Cover = await Gfriends.FindAsync(item.Name, cancellationToken); 138 | if (string.IsNullOrWhiteSpace(index.Cover)) 139 | return list; 140 | } 141 | 142 | if (index.Cover.IsWebUrl()) 143 | { 144 | await Add(index.Cover, ImageType.Primary); 145 | await Add(index.Cover, ImageType.Backdrop); 146 | } 147 | 148 | if (index.Samples?.Any() == true) 149 | { 150 | foreach (var url in index.Samples.Where(o => o.IsWebUrl())) 151 | await Add(url, ImageType.Thumb); 152 | } 153 | } 154 | 155 | return list; 156 | } 157 | 158 | public System.Collections.Generic.IEnumerable GetSupportedImages(BaseItem item) 159 | => new[] { ImageType.Primary, ImageType.Backdrop, ImageType.Thumb }; 160 | 161 | public bool Supports(BaseItem item) => item is Movie || item is Person; 162 | } 163 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/JavPersonTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Emby.Plugins.JavScraper.Scrapers; 7 | using Emby.Plugins.JavScraper.Services; 8 | using MediaBrowser.Common.Configuration; 9 | using MediaBrowser.Controller.Entities; 10 | using MediaBrowser.Controller.Library; 11 | using MediaBrowser.Controller.Providers; 12 | using MediaBrowser.Model.Entities; 13 | using MediaBrowser.Model.IO; 14 | using MediaBrowser.Model.Tasks; 15 | using MediaBrowser.Model.Serialization; 16 | 17 | #if __JELLYFIN__ 18 | using Microsoft.Extensions.Logging; 19 | #else 20 | using MediaBrowser.Model.Logging; 21 | #endif 22 | 23 | namespace Emby.Plugins.JavScraper 24 | { 25 | public class JavPersonTask : IScheduledTask 26 | { 27 | private readonly ILibraryManager libraryManager; 28 | private readonly IJsonSerializer _jsonSerializer; 29 | private readonly ImageProxyService imageProxyService; 30 | private readonly IProviderManager providerManager; 31 | private readonly IFileSystem fileSystem; 32 | private readonly ILogger _logger; 33 | 34 | public Gfriends Gfriends { get; } 35 | 36 | public JavPersonTask( 37 | #if __JELLYFIN__ 38 | ILoggerFactory logManager, 39 | #else 40 | ILogManager logManager, 41 | ImageProxyService imageProxyService, 42 | Gfriends gfriends, 43 | #endif 44 | ILibraryManager libraryManager, 45 | IJsonSerializer _jsonSerializer, IApplicationPaths appPaths, 46 | 47 | IProviderManager providerManager, 48 | IFileSystem fileSystem) 49 | { 50 | _logger = logManager.CreateLogger(); 51 | this.libraryManager = libraryManager; 52 | this._jsonSerializer = _jsonSerializer; 53 | #if __JELLYFIN__ 54 | imageProxyService = Plugin.Instance.ImageProxyService; 55 | Gfriends = new Gfriends(logManager, _jsonSerializer); 56 | #else 57 | this.imageProxyService = imageProxyService; 58 | Gfriends = gfriends; 59 | #endif 60 | this.providerManager = providerManager; 61 | this.fileSystem = fileSystem; 62 | } 63 | 64 | public string Name => Plugin.NAME + ": 采集缺失的女优头像和信息"; 65 | public string Key => Plugin.NAME + "-Actress"; 66 | public string Description => "采集缺失的女优头像和信息"; 67 | public string Category => "JavScraper"; 68 | 69 | public IEnumerable GetDefaultTriggers() 70 | { 71 | var t = new TaskTriggerInfo 72 | { 73 | Type = TaskTriggerInfo.TriggerWeekly, 74 | TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, 75 | MaxRuntimeTicks = TimeSpan.FromHours(3).Ticks, 76 | DayOfWeek = DayOfWeek.Monday 77 | }; 78 | return new[] { t }; 79 | } 80 | 81 | public async Task Execute(CancellationToken cancellationToken, IProgress progress) 82 | { 83 | _logger.Info($"Running..."); 84 | progress.Report(0); 85 | 86 | IDirectoryService ds = default; 87 | 88 | var dstype = typeof(DirectoryService); 89 | var cr = dstype.GetConstructors().Where(o => o.IsPublic && o.IsStatic == false).OrderByDescending(o => o.GetParameters().Length).FirstOrDefault(); 90 | if (cr.GetParameters().Length == 1) 91 | ds = cr.Invoke(new[] { fileSystem }) as IDirectoryService; 92 | else 93 | ds = cr.Invoke(new object[] { _logger, fileSystem }) as IDirectoryService; 94 | 95 | var query = new InternalItemsQuery() 96 | { 97 | IncludeItemTypes = new[] { nameof(Person) }, 98 | PersonTypes = new[] { PersonType.Actor } 99 | }; 100 | 101 | var persons = libraryManager.GetItemList(query)?.ToList(); 102 | 103 | if (persons?.Any() != true) 104 | { 105 | progress.Report(100); 106 | return; 107 | } 108 | persons.RemoveAll(o => !(o is Person)); 109 | 110 | for (int i = 0; i < persons.Count; ++i) 111 | { 112 | var person = persons[i]; 113 | 114 | MetadataRefreshMode imageRefreshMode = 0; 115 | MetadataRefreshMode metadataRefreshMode = 0; 116 | 117 | if (!person.HasImage(ImageType.Primary)) 118 | imageRefreshMode = MetadataRefreshMode.Default; 119 | if (string.IsNullOrEmpty(person.Overview)) 120 | metadataRefreshMode = MetadataRefreshMode.FullRefresh; 121 | 122 | if (imageRefreshMode == 0 && metadataRefreshMode == 0) 123 | continue; 124 | 125 | var options = new MetadataRefreshOptions(ds) 126 | { 127 | ImageRefreshMode = imageRefreshMode, 128 | MetadataRefreshMode = metadataRefreshMode 129 | }; 130 | 131 | try 132 | { 133 | await person.RefreshMetadata(options, cancellationToken); 134 | } 135 | catch { } 136 | progress.Report(i * 1.0 / persons.Count * 100); 137 | } 138 | 139 | progress.Report(100); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Configuration; 2 | using MediaBrowser.Common.Configuration; 3 | using MediaBrowser.Common.Plugins; 4 | using MediaBrowser.Model.Drawing; 5 | 6 | #if __JELLYFIN__ 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Emby.Plugins.JavScraper.Services; 10 | #else 11 | using MediaBrowser.Model.Logging; 12 | #endif 13 | 14 | using MediaBrowser.Model.Plugins; 15 | using MediaBrowser.Model.Serialization; 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using Emby.Plugins.JavScraper.Data; 20 | using Emby.Plugins.JavScraper.Scrapers; 21 | using System.Linq; 22 | using System.Collections.ObjectModel; 23 | using MediaBrowser.Common; 24 | using MediaBrowser.Controller; 25 | 26 | namespace Emby.Plugins.JavScraper 27 | { 28 | public class Plugin 29 | : BasePlugin, IHasWebPages 30 | #if !__JELLYFIN__ 31 | , IHasThumbImage 32 | #endif 33 | { 34 | /// 35 | /// 名称 36 | /// 37 | public const string NAME = "JavScraper"; 38 | 39 | private ILogger logger; 40 | 41 | /// 42 | /// 数据库 43 | /// 44 | public ApplicationDbContext db { get; } 45 | 46 | /// 47 | /// 全部的刮削器 48 | /// 49 | public ReadOnlyCollection Scrapers { get; } 50 | 51 | #if __JELLYFIN__ 52 | 53 | /// 54 | /// 图片服务 55 | /// 56 | public ImageProxyService ImageProxyService { get; } 57 | 58 | /// 59 | /// 翻译服务 60 | /// 61 | public TranslationService TranslationService { get; } 62 | 63 | #endif 64 | 65 | /// 66 | /// COPY TO /volume1/@appstore/EmbyServer/releases/4.3.1.0/plugins 67 | /// 68 | /// 69 | /// 70 | /// 71 | public Plugin(IApplicationPaths applicationPaths, IApplicationHost applicationHost, IXmlSerializer xmlSerializer, 72 | IServerApplicationHost serverApplicationHost, 73 | #if __JELLYFIN__ 74 | IServiceProvider serviceProvider, 75 | ILoggerFactory logManager 76 | #else 77 | ILogManager logManager 78 | #endif 79 | ) : base(applicationPaths, xmlSerializer) 80 | { 81 | Instance = this; 82 | logger = logManager.CreateLogger(); 83 | logger?.Info($"{Name} - Loaded."); 84 | db = ApplicationDbContext.Create(applicationPaths); 85 | Scrapers = applicationHost.GetExports(false).Where(o => o != null).ToList().AsReadOnly(); 86 | 87 | #if __JELLYFIN__ 88 | ImageProxyService = new ImageProxyService(serverApplicationHost, serviceProvider.GetService(), logManager.CreateLogger(), 89 | serviceProvider.GetService(), applicationPaths); 90 | TranslationService = new TranslationService(serviceProvider.GetService(), logManager.CreateLogger()); 91 | #endif 92 | } 93 | 94 | public override Guid Id => new Guid("0F34B81A-4AF7-4719-9958-4CB8F680E7C6"); 95 | 96 | public override string Name => NAME; 97 | 98 | public override string Description => "Jav Scraper"; 99 | 100 | public static Plugin Instance { get; private set; } 101 | 102 | public IEnumerable GetPages() 103 | { 104 | var type = GetType(); 105 | string prefix = ""; 106 | #if __JELLYFIN__ 107 | prefix = "Jellyfin."; 108 | #endif 109 | return new[] 110 | { 111 | new PluginPageInfo 112 | { 113 | Name = Name, 114 | EmbeddedResourcePath = $"{type.Namespace}.Configuration.{prefix}ConfigPage.html", 115 | EnableInMainMenu = true, 116 | MenuSection = "server", 117 | MenuIcon = "theaters", 118 | DisplayName = "Jav Scraper", 119 | }, 120 | new PluginPageInfo 121 | { 122 | Name = "JavOrganize", 123 | EmbeddedResourcePath = $"{type.Namespace}.Configuration.{prefix}JavOrganizationConfigPage.html", 124 | EnableInMainMenu = true, 125 | MenuSection = "server", 126 | MenuIcon = "theaters", 127 | DisplayName = "Jav Organize", 128 | } 129 | }; 130 | } 131 | 132 | public Stream GetThumbImage() 133 | { 134 | var type = GetType(); 135 | return type.Assembly.GetManifestResourceStream($"{type.Namespace}.thumb.png"); 136 | } 137 | 138 | public ImageFormat ThumbImageFormat => ImageFormat.Png; 139 | 140 | public override void SaveConfiguration() 141 | { 142 | Configuration.ConfigurationVersion = DateTime.Now.Ticks; 143 | base.SaveConfiguration(); 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/AVSOX.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using HtmlAgilityPack; 3 | #if __JELLYFIN__ 4 | using Microsoft.Extensions.Logging; 5 | #else 6 | using MediaBrowser.Model.Logging; 7 | #endif 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net.Http; 12 | using System.Threading.Tasks; 13 | 14 | namespace Emby.Plugins.JavScraper.Scrapers 15 | { 16 | /// 17 | /// https://avsox.host/cn/search/032416_525 18 | /// https://avsox.host/cn/movie/77f594342b5e2afe 19 | /// 20 | public class AVSOX : AbstractScraper 21 | { 22 | /// 23 | /// 适配器名称 24 | /// 25 | public override string Name => "AVSOX"; 26 | 27 | /// 28 | /// 构造 29 | /// 30 | /// 31 | public AVSOX( 32 | #if __JELLYFIN__ 33 | ILoggerFactory logManager 34 | #else 35 | ILogManager logManager 36 | #endif 37 | ) 38 | : base("https://avsox.website/", logManager.CreateLogger()) 39 | { 40 | } 41 | 42 | /// 43 | /// 检查关键字是否符合 44 | /// 45 | /// 46 | /// 47 | public override bool CheckKey(string key) 48 | => true; 49 | 50 | /// 51 | /// 获取列表 52 | /// 53 | /// 关键字 54 | /// 55 | protected override async Task> DoQyery(List ls, string key) 56 | { 57 | ///https://javdb.com/search?q=ADN-106&f=all 58 | var doc = await GetHtmlDocumentAsync($"/cn/search/{key}"); 59 | if (doc != null) 60 | ParseIndex(ls, doc); 61 | 62 | SortIndex(key, ls); 63 | return ls; 64 | } 65 | 66 | /// 67 | /// 解析列表 68 | /// 69 | /// 70 | /// 71 | /// 72 | protected override List ParseIndex(List ls, HtmlDocument doc) 73 | { 74 | if (doc == null) 75 | return ls; 76 | var nodes = doc.DocumentNode.SelectNodes("//div[@class='item']/a"); 77 | if (nodes?.Any() != true) 78 | return ls; 79 | 80 | foreach (var node in nodes) 81 | { 82 | var url = node.GetAttributeValue("href", null); 83 | if (string.IsNullOrWhiteSpace(url)) 84 | continue; 85 | var m = new JavVideoIndex() { Provider = Name, Url = url }; 86 | 87 | var img = node.SelectSingleNode(".//div[@class='photo-frame']//img"); 88 | if (img != null) 89 | { 90 | m.Cover = img.GetAttributeValue("src", null); 91 | m.Title = img.GetAttributeValue("title", null); 92 | } 93 | var dates = node.SelectNodes(".//date"); 94 | if (dates?.Count >= 1) 95 | m.Num = dates[0].InnerText.Trim(); 96 | if (dates?.Count >= 2) 97 | m.Date = dates[1].InnerText.Trim(); 98 | if (string.IsNullOrWhiteSpace(m.Num)) 99 | continue; 100 | ls.Add(m); 101 | } 102 | 103 | return ls; 104 | } 105 | 106 | /// 107 | /// 获取详情 108 | /// 109 | /// 地址 110 | /// 111 | public override async Task Get(string url) 112 | { 113 | //https://www.javbus.cloud/ABP-933 114 | var doc = await GetHtmlDocumentAsync(url); 115 | if (doc == null) 116 | return null; 117 | 118 | var node = doc.DocumentNode.SelectSingleNode("//div[@class='container']/h3/.."); 119 | if (node == null) 120 | return null; 121 | 122 | var dic = new Dictionary(); 123 | var nodes = node.SelectNodes(".//*[@class='header']"); 124 | foreach (var n in nodes) 125 | { 126 | var next = n.NextSibling; 127 | while (next != null && string.IsNullOrWhiteSpace(next.InnerText)) 128 | next = next.NextSibling; 129 | if (next != null) 130 | dic[n.InnerText.Trim()] = next.InnerText.Trim(); 131 | } 132 | 133 | string GetValue(string _key) 134 | => dic.Where(o => o.Key.Contains(_key)).Select(o => o.Value).FirstOrDefault(); 135 | 136 | var genres = node.SelectNodes(".//span[@class='genre']")? 137 | .Select(o => o.InnerText.Trim()).ToList(); 138 | 139 | var actors = node.SelectNodes(".//*[@class='avatar-box']")? 140 | .Select(o => o.InnerText.Trim()).ToList(); 141 | 142 | var samples = node.SelectNodes(".//a[@class='sample-box']")? 143 | .Select(o => o.GetAttributeValue("href", null)).Where(o => o != null).ToList(); 144 | var m = new JavVideo() 145 | { 146 | Provider = Name, 147 | Url = url, 148 | Title = node.SelectSingleNode("./h3")?.InnerText?.Trim(), 149 | Cover = node.SelectSingleNode(".//a[@class='bigImage']")?.GetAttributeValue("href", null), 150 | Num = GetValue("识别码"), 151 | Date = GetValue("发行时间"), 152 | Runtime = GetValue("长度"), 153 | Maker = GetValue("发行商"), 154 | Studio = GetValue("制作商"), 155 | Set = GetValue("系列"), 156 | Director = GetValue("导演"), 157 | //Plot = node.SelectSingleNode("./h3")?.InnerText, 158 | Genres = genres, 159 | Actors = actors, 160 | Samples = samples, 161 | }; 162 | 163 | m.Plot = await GetDmmPlot(m.Num); 164 | //去除标题中的番号 165 | if (string.IsNullOrWhiteSpace(m.Num) == false && m.Title?.StartsWith(m.Num, StringComparison.OrdinalIgnoreCase) == true) 166 | m.Title = m.Title.Substring(m.Num.Length).Trim(); 167 | 168 | return m; 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/FC2.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using HtmlAgilityPack; 3 | #if __JELLYFIN__ 4 | using Microsoft.Extensions.Logging; 5 | #else 6 | using MediaBrowser.Model.Logging; 7 | #endif 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net.Http; 12 | using System.Text.RegularExpressions; 13 | using System.Threading.Tasks; 14 | 15 | namespace Emby.Plugins.JavScraper.Scrapers 16 | { 17 | /// 18 | /// https://fc2club.net/html/FC2-1249328.html 19 | /// 20 | public class FC2 : AbstractScraper 21 | { 22 | /// 23 | /// 适配器名称 24 | /// 25 | public override string Name => "FC2"; 26 | 27 | private static Regex regexDate = new Regex(@"(?[\d]{4}[-/][\d]{2}[-/][\d]{2})", RegexOptions.Compiled | RegexOptions.IgnoreCase); 28 | 29 | private static Regex regexFC2 = new Regex(@"FC2-*(PPV|)-(?[\d]{2,10})($|[^\d])", RegexOptions.IgnoreCase | RegexOptions.Compiled); 30 | 31 | /// 32 | /// 构造 33 | /// 34 | /// 35 | public FC2( 36 | #if __JELLYFIN__ 37 | ILoggerFactory logManager 38 | #else 39 | ILogManager logManager 40 | #endif 41 | ) 42 | : base("https://fc2club.net/", logManager.CreateLogger()) 43 | { 44 | } 45 | 46 | /// 47 | /// 检查关键字是否符合 48 | /// 49 | /// 50 | /// 51 | public override bool CheckKey(string key) 52 | => JavIdRecognizer.FC2(key) != null; 53 | 54 | public override Task> Query(string key) 55 | { 56 | var m = regexFC2.Match(key); 57 | if (m.Success == false) 58 | return Task.FromResult(new List()); 59 | var id = m.Groups["id"].Value; 60 | return DoQyery(new List(), id); 61 | } 62 | 63 | /// 64 | /// 获取列表 65 | /// 66 | /// 关键字 67 | /// 68 | protected override async Task> DoQyery(List ls, string key) 69 | { 70 | var item = await GetById(key); 71 | if (item != null) 72 | { 73 | ls.Add(new JavVideoIndex() 74 | { 75 | Cover = item.Cover, 76 | Date = item.Date, 77 | Num = item.Num, 78 | Provider = item.Provider, 79 | Title = item.Title, 80 | Url = item.Url 81 | }); 82 | } 83 | return ls; 84 | } 85 | 86 | /// 87 | /// 无效方法 88 | /// 89 | /// 90 | /// 91 | /// 92 | protected override List ParseIndex(List ls, HtmlDocument doc) 93 | { 94 | throw new NotImplementedException(); 95 | } 96 | 97 | /// 98 | /// 获取详情 99 | /// 100 | /// 地址 101 | /// 102 | public override async Task Get(string url) 103 | { 104 | var m = regexFC2.Match(url); 105 | if (m.Success == false) 106 | return null; 107 | return await GetById(m.Groups["id"].Value); 108 | } 109 | 110 | /// 111 | /// 获取详情 112 | /// 113 | /// 地址 114 | /// 115 | private async Task GetById(string id) 116 | { 117 | //https://adult.contents.fc2.com/article/1252526/ 118 | //https://fc2club.net/html/FC2-1252526.html 119 | var url = $"/html/FC2-{id}.html"; 120 | var doc = await GetHtmlDocumentAsync(url); 121 | if (doc == null) 122 | return null; 123 | 124 | var node = doc.DocumentNode.SelectSingleNode("//div[@class='show-top-grids']"); 125 | if (node == null) 126 | return null; 127 | 128 | var doc2 = GetHtmlDocumentAsync($"https://adult.contents.fc2.com/article/{id}/"); 129 | 130 | var dic = new Dictionary(); 131 | var nodes = node.SelectNodes(".//h5/strong/.."); 132 | foreach (var n in nodes) 133 | { 134 | var name = n.SelectSingleNode("./strong")?.InnerText?.Trim(); 135 | if (string.IsNullOrWhiteSpace(name)) 136 | continue; 137 | //尝试获取 a 标签的内容 138 | var aa = n.SelectNodes("./a"); 139 | var value = aa?.Any() == true ? string.Join(", ", aa.Select(o => o.InnerText.Trim()).Where(o => string.IsNullOrWhiteSpace(o) == false && !o.Contains("本资源"))) 140 | : n.InnerText?.Split(':').Last(); 141 | 142 | if (string.IsNullOrWhiteSpace(value) == false) 143 | dic[name] = value; 144 | } 145 | 146 | string GetValue(string _key) 147 | => dic.Where(o => o.Key.Contains(_key)).Select(o => o.Value).FirstOrDefault(); 148 | 149 | var genres = GetValue("影片标签")?.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries).ToList(); 150 | 151 | var actors = GetValue("女优名字")?.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries).ToList(); 152 | 153 | string getDate() 154 | { 155 | var t = doc2.GetAwaiter().GetResult()?.DocumentNode.SelectSingleNode("//div[@class='items_article_Releasedate']")?.InnerText; 156 | if (string.IsNullOrWhiteSpace(t)) 157 | return null; 158 | var dm = regexDate.Match(t); 159 | if (dm.Success == false) 160 | return null; 161 | return dm.Groups["date"].Value.Replace('/', '-'); 162 | } 163 | 164 | float? GetCommunityRating() 165 | { 166 | var value = GetValue("影片评分"); 167 | if (string.IsNullOrWhiteSpace(value)) 168 | return null; 169 | var m = Regex.Match(value, @"(?[\d.]+)"); 170 | if (m.Success == false) 171 | return null; 172 | if (float.TryParse(m.Groups["rating"].Value, out var rating)) 173 | return rating / 10.0f; 174 | return null; 175 | } 176 | 177 | var samples = node.SelectNodes("//ul[@class='slides']/li/img")? 178 | .Select(o => o.GetAttributeValue("src", null)).Where(o => o != null).Select(o => new Uri(client.BaseAddress, o).ToString()).ToList(); 179 | var m = new JavVideo() 180 | { 181 | Provider = Name, 182 | Url = url, 183 | Title = node.SelectSingleNode(".//h3")?.InnerText?.Trim(), 184 | Cover = samples?.FirstOrDefault(), 185 | Num = $"FC2-{id}", 186 | Date = getDate(), 187 | //Runtime = GetValue("収録時間"), 188 | Maker = GetValue("卖家信息"), 189 | Studio = GetValue("卖家信息"), 190 | Set = Name, 191 | //Director = GetValue("シリーズ"), 192 | //Plot = node.SelectSingleNode("//p[@class='txt introduction']")?.InnerText, 193 | Genres = genres, 194 | Actors = actors, 195 | Samples = samples, 196 | CommunityRating = GetCommunityRating(), 197 | }; 198 | //去除标题中的番号 199 | if (string.IsNullOrWhiteSpace(m.Num) == false && m.Title?.StartsWith(m.Num, StringComparison.OrdinalIgnoreCase) == true) 200 | m.Title = m.Title.Substring(m.Num.Length).Trim(); 201 | 202 | return m; 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/Gfriends.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using MediaBrowser.Model.Serialization; 3 | 4 | #if __JELLYFIN__ 5 | using Microsoft.Extensions.Logging; 6 | #else 7 | using MediaBrowser.Model.Logging; 8 | #endif 9 | 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | namespace Emby.Plugins.JavScraper.Scrapers 17 | { 18 | /// 19 | /// 头像 20 | /// 21 | public class Gfriends 22 | { 23 | protected HttpClientEx client; 24 | protected ILogger log; 25 | private readonly IJsonSerializer _jsonSerializer; 26 | 27 | /// 28 | /// 适配器名称 29 | /// 30 | public string Name => "gfriends"; 31 | 32 | private FileTreeModel tree; 33 | private DateTime last = DateTime.Now.AddDays(-1); 34 | private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); 35 | private const string base_url = "https://raw.githubusercontent.com/xinxin8816/gfriends/master/"; 36 | 37 | public Gfriends( 38 | #if __JELLYFIN__ 39 | ILoggerFactory logManager 40 | #else 41 | ILogManager logManager 42 | #endif 43 | , IJsonSerializer jsonSerializer) 44 | { 45 | client = new HttpClientEx(client => client.BaseAddress = new Uri(base_url)); 46 | this.log = logManager.CreateLogger(); 47 | this._jsonSerializer = jsonSerializer; 48 | } 49 | 50 | /// 51 | /// 查找女优的头像地址 52 | /// 53 | /// 女优姓名 54 | /// 55 | /// 56 | public async Task FindAsync(string name, CancellationToken cancelationToken) 57 | { 58 | await locker.WaitAsync(cancelationToken); 59 | try 60 | { 61 | if (tree == null || (DateTime.Now - last).TotalHours > 1) 62 | { 63 | var json = await client.GetStringAsync("Filetree.json"); 64 | tree = _jsonSerializer.DeserializeFromString(json); 65 | last = DateTime.Now; 66 | tree.Content = tree.Content.OrderBy(o => o.Key).ToDictionary(o => o.Key, o => o.Value); 67 | } 68 | } 69 | catch (Exception ex) 70 | { 71 | log.Error(ex.Message); 72 | } 73 | finally 74 | { 75 | locker.Release(); 76 | } 77 | 78 | if (tree?.Content?.Any() != true) 79 | return null; 80 | 81 | return tree.Find(name); 82 | } 83 | 84 | /// 85 | /// 树模型 86 | /// 87 | public class FileTreeModel 88 | { 89 | /// 90 | /// 内容 91 | /// 92 | public Dictionary> Content { get; set; } 93 | 94 | /// 95 | /// 查找图片 96 | /// 97 | /// 98 | /// 99 | public string Find(string name) 100 | { 101 | if (string.IsNullOrWhiteSpace(name)) 102 | return null; 103 | 104 | var key = $"{name.Trim()}."; 105 | 106 | foreach (var dd in Content) 107 | { 108 | foreach (var d in dd.Value) 109 | { 110 | if (d.Key.StartsWith(key)) 111 | return $"{base_url}Content/{dd.Key}/{d.Value}"; 112 | } 113 | } 114 | 115 | return null; 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/Jav123.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using HtmlAgilityPack; 3 | #if __JELLYFIN__ 4 | using Microsoft.Extensions.Logging; 5 | #else 6 | using MediaBrowser.Model.Logging; 7 | #endif 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net.Http; 12 | using System.Threading.Tasks; 13 | 14 | namespace Emby.Plugins.JavScraper.Scrapers 15 | { 16 | /// 17 | /// https://www.jav321.com/ 18 | /// 19 | public class Jav123 : AbstractScraper 20 | { 21 | /// 22 | /// 适配器名称 23 | /// 24 | public override string Name => "Jav123"; 25 | 26 | /// 27 | /// 构造 28 | /// 29 | /// 30 | public Jav123( 31 | #if __JELLYFIN__ 32 | ILoggerFactory logManager 33 | #else 34 | ILogManager logManager 35 | #endif 36 | ) 37 | : base("https://www.jav321.com/", logManager.CreateLogger()) 38 | { 39 | } 40 | 41 | /// 42 | /// 检查关键字是否符合 43 | /// 44 | /// 45 | /// 46 | public override bool CheckKey(string key) 47 | => JavIdRecognizer.FC2(key) == null; 48 | 49 | /// 50 | /// 获取列表 51 | /// 52 | /// 关键字 53 | /// 54 | protected override async Task> DoQyery(List ls, string key) 55 | { 56 | ///https://www.jav321.com/search 57 | ///POST sn=key 58 | var doc = await GetHtmlDocumentByPostAsync($"/search", new Dictionary() { ["sn"] = key }); 59 | if (doc != null) 60 | { 61 | var video = await ParseVideo(null, doc); 62 | if (video != null) 63 | ls.Add(video); 64 | } 65 | 66 | SortIndex(key, ls); 67 | return ls; 68 | } 69 | 70 | /// 71 | /// 不用了 72 | /// 73 | /// 74 | /// 75 | /// 76 | protected override List ParseIndex(List ls, HtmlDocument doc) 77 | { 78 | throw new NotImplementedException(); 79 | } 80 | 81 | /// 82 | /// 获取详情 83 | /// 84 | /// 地址 85 | /// 86 | public override async Task Get(string url) 87 | { 88 | //https://javdb.com/v/BzbA6 89 | var doc = await GetHtmlDocumentAsync(url); 90 | if (doc == null) 91 | return null; 92 | 93 | return await ParseVideo(url, doc); 94 | } 95 | 96 | private async Task ParseVideo(string url, HtmlDocument doc) 97 | { 98 | var node = doc.DocumentNode.SelectSingleNode("//div[@class='panel-heading']/h3/../.."); 99 | if (node == null) 100 | return null; 101 | var nodes = node.SelectNodes(".//b"); 102 | if (nodes?.Any() != true) 103 | return null; 104 | 105 | if (string.IsNullOrWhiteSpace(url)) 106 | { 107 | url = doc.DocumentNode.SelectSingleNode("//li/a[contains(text(),'简体中文')]")?.GetAttributeValue("href", null); 108 | if (url?.StartsWith("//") == true) 109 | url = $"https:{url}"; 110 | } 111 | 112 | var dic = new Dictionary(); 113 | foreach (var n in nodes) 114 | { 115 | var name = n.InnerText.Trim(); 116 | if (string.IsNullOrWhiteSpace(name)) 117 | continue; 118 | var arr = new List(); 119 | 120 | var next = n.NextSibling; 121 | while (next != null && next.Name != "b") 122 | { 123 | arr.Add(next.InnerText); 124 | next = next.NextSibling; 125 | } 126 | if (arr.Count == 0) 127 | continue; 128 | 129 | var value = string.Join(", ", arr.Select(o => o.Replace(" ", " ").Trim(": ".ToArray())).Where(o => string.IsNullOrWhiteSpace(o) == false)); 130 | 131 | if (string.IsNullOrWhiteSpace(value)) 132 | continue; 133 | 134 | dic[name] = value; 135 | } 136 | 137 | string GetValue(string _key) 138 | => dic.Where(o => o.Key.Contains(_key)).Select(o => o.Value).FirstOrDefault(); 139 | 140 | string GetCover() 141 | { 142 | var img = node.SelectSingleNode(".//*[@id='vjs_sample_player']")?.GetAttributeValue("poster", null); 143 | if (string.IsNullOrWhiteSpace(img) == false) 144 | return img; 145 | if (string.IsNullOrWhiteSpace(img) == false) 146 | return img; 147 | img = node.SelectSingleNode(".//*[@id='video-player']")?.GetAttributeValue("poster", null); 148 | img = doc.DocumentNode.SelectSingleNode("//img[@class='img-responsive']")?.GetAttributeValue("src", null); 149 | if (string.IsNullOrWhiteSpace(img) == false) 150 | return img; 151 | return img; 152 | } 153 | 154 | List GetGenres() 155 | { 156 | var v = GetValue("ジャンル"); 157 | if (string.IsNullOrWhiteSpace(v)) 158 | return null; 159 | return v.Split(',').Select(o => o.Trim()).Distinct().ToList(); 160 | } 161 | 162 | List GetActors() 163 | { 164 | var v = GetValue("出演者"); 165 | if (string.IsNullOrWhiteSpace(v)) 166 | return null; 167 | var ac = v.Split(',').Select(o => o.Trim()).Distinct().ToList(); 168 | return ac; 169 | } 170 | List GetSamples() 171 | { 172 | return doc.DocumentNode.SelectNodes("//a[contains(@href,'snapshot')]/img") 173 | ?.Select(o => o.GetAttributeValue("src", null)) 174 | .Where(o => string.IsNullOrWhiteSpace(o) == false).ToList(); 175 | } 176 | 177 | var m = new JavVideo() 178 | { 179 | Provider = Name, 180 | Url = url, 181 | Title = node.SelectSingleNode(".//h3/text()")?.InnerText?.Trim(), 182 | Cover = GetCover(), 183 | Num = GetValue("品番")?.ToUpper(), 184 | Date = GetValue("配信開始日"), 185 | Runtime = GetValue("収録時間"), 186 | Maker = GetValue("メーカー"), 187 | Studio = GetValue("メーカー"), 188 | Set = GetValue("シリーズ"), 189 | Director = GetValue("导演"), 190 | Genres = GetGenres(), 191 | Actors = GetActors(), 192 | Samples = GetSamples(), 193 | Plot = node.SelectSingleNode("./div[@class='panel-body']/div[last()]")?.InnerText?.Trim(), 194 | }; 195 | if (string.IsNullOrWhiteSpace(m.Plot)) 196 | m.Plot = await GetDmmPlot(m.Num); 197 | ////去除标题中的番号 198 | if (string.IsNullOrWhiteSpace(m.Num) == false && m.Title?.StartsWith(m.Num, StringComparison.OrdinalIgnoreCase) == true) 199 | m.Title = m.Title.Substring(m.Num.Length).Trim(); 200 | 201 | return m; 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/JavBus.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using HtmlAgilityPack; 3 | #if __JELLYFIN__ 4 | using Microsoft.Extensions.Logging; 5 | #else 6 | using MediaBrowser.Model.Logging; 7 | #endif 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net.Http; 12 | using System.Threading.Tasks; 13 | 14 | namespace Emby.Plugins.JavScraper.Scrapers 15 | { 16 | /// 17 | /// https://www.javbus.com/BIJN-172 18 | /// 19 | public class JavBus : AbstractScraper 20 | { 21 | /// 22 | /// 适配器名称 23 | /// 24 | public override string Name => "JavBus"; 25 | 26 | /// 27 | /// 构造 28 | /// 29 | /// 30 | public JavBus( 31 | #if __JELLYFIN__ 32 | ILoggerFactory logManager 33 | #else 34 | ILogManager logManager 35 | #endif 36 | ) 37 | : base("https://www.javbus.com/", logManager.CreateLogger()) 38 | { 39 | } 40 | 41 | /// 42 | /// 检查关键字是否符合 43 | /// 44 | /// 45 | /// 46 | public override bool CheckKey(string key) 47 | => JavIdRecognizer.FC2(key) == null; 48 | 49 | /// 50 | /// 获取列表 51 | /// 52 | /// 关键字 53 | /// 54 | protected override async Task> DoQyery(List ls, string key) 55 | { 56 | //https://www.javbus.cloud/search/33&type=1 57 | //https://www.javbus.cloud/uncensored/search/33&type=0&parent=uc 58 | var doc = await GetHtmlDocumentAsync($"/search/{key}&type=1"); 59 | if (doc != null) 60 | { 61 | ParseIndex(ls, doc); 62 | 63 | //判断是否有 无码的影片 64 | var node = doc.DocumentNode.SelectSingleNode("//a[contains(@href,'/uncensored/search/')]"); 65 | if (node != null) 66 | { 67 | var t = node.InnerText; 68 | var ii = t.Split('/'); 69 | //没有 70 | if (ii.Length > 2 && ii[1].Trim().StartsWith("0")) 71 | return ls; 72 | } 73 | } 74 | doc = await GetHtmlDocumentAsync($"/uncensored/search/{key}&type=1"); 75 | ParseIndex(ls, doc); 76 | 77 | SortIndex(key, ls); 78 | return ls; 79 | } 80 | 81 | /// 82 | /// 解析列表 83 | /// 84 | /// 85 | /// 86 | /// 87 | protected override List ParseIndex(List ls, HtmlDocument doc) 88 | { 89 | if (doc == null) 90 | return ls; 91 | var nodes = doc.DocumentNode.SelectNodes("//a[@class='movie-box']"); 92 | if (nodes?.Any() != true) 93 | return ls; 94 | 95 | foreach (var node in nodes) 96 | { 97 | var url = node.GetAttributeValue("href", null); 98 | if (string.IsNullOrWhiteSpace(url)) 99 | continue; 100 | var m = new JavVideoIndex() { Provider = Name, Url = url }; 101 | 102 | var img = node.SelectSingleNode(".//div[@class='photo-frame']//img"); 103 | if (img != null) 104 | { 105 | m.Cover = img.GetAttributeValue("src", null); 106 | m.Title = img.GetAttributeValue("title", null); 107 | } 108 | var dates = node.SelectNodes(".//date"); 109 | if (dates?.Count >= 1) 110 | m.Num = dates[0].InnerText.Trim(); 111 | if (dates?.Count >= 2) 112 | m.Date = dates[1].InnerText.Trim(); 113 | 114 | if (string.IsNullOrWhiteSpace(m.Num)) 115 | continue; 116 | ls.Add(m); 117 | 118 | } 119 | 120 | return ls; 121 | } 122 | 123 | /// 124 | /// 获取详情 125 | /// 126 | /// 地址 127 | /// 128 | public override async Task Get(string url) 129 | { 130 | //https://www.javbus.cloud/ABP-933 131 | var doc = await GetHtmlDocumentAsync(url); 132 | if (doc == null) 133 | return null; 134 | 135 | var node = doc.DocumentNode.SelectSingleNode("//div[@class='container']/h3/.."); 136 | if (node == null) 137 | return null; 138 | 139 | var dic = new Dictionary(); 140 | var nodes = node.SelectNodes(".//span[@class='header']"); 141 | foreach (var n in nodes) 142 | { 143 | var next = n.NextSibling; 144 | while (next != null && string.IsNullOrWhiteSpace(next.InnerText)) 145 | next = next.NextSibling; 146 | if (next != null) 147 | dic[n.InnerText.Trim()] = next.InnerText.Trim(); 148 | } 149 | 150 | string GetValue(string _key) 151 | => dic.Where(o => o.Key.Contains(_key)).Select(o => o.Value).FirstOrDefault(); 152 | 153 | var genres = node.SelectNodes(".//span[@class='genre']")? 154 | .Select(o => o.InnerText.Trim()).ToList(); 155 | 156 | var actors = node.SelectNodes(".//div[@class='star-name']")? 157 | .Select(o => o.InnerText.Trim()).ToList(); 158 | 159 | var samples = node.SelectNodes(".//a[@class='sample-box']")? 160 | .Select(o => o.GetAttributeValue("href", null)).Where(o => o != null).ToList(); 161 | var m = new JavVideo() 162 | { 163 | Provider = Name, 164 | Url = url, 165 | Title = node.SelectSingleNode("./h3")?.InnerText?.Trim(), 166 | Cover = node.SelectSingleNode(".//a[@class='bigImage']")?.GetAttributeValue("href", null), 167 | Num = GetValue("識別碼"), 168 | Date = GetValue("發行日期"), 169 | Runtime = GetValue("長度"), 170 | Maker = GetValue("發行商"), 171 | Studio = GetValue("製作商"), 172 | Set = GetValue("系列"), 173 | Director = GetValue("導演"), 174 | //Plot = node.SelectSingleNode("./h3")?.InnerText, 175 | Genres = genres, 176 | Actors = actors, 177 | Samples = samples, 178 | }; 179 | 180 | m.Plot = await GetDmmPlot(m.Num); 181 | //去除标题中的番号 182 | if (string.IsNullOrWhiteSpace(m.Num) == false && m.Title?.StartsWith(m.Num, StringComparison.OrdinalIgnoreCase) == true) 183 | m.Title = m.Title.Substring(m.Num.Length).Trim(); 184 | 185 | return m; 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/JavPersonIndex.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Entities; 2 | using System.Collections.Generic; 3 | 4 | namespace Emby.Plugins.JavScraper.Scrapers 5 | { 6 | public class JavPersonIndex 7 | { 8 | /// 9 | /// 适配器 10 | /// 11 | public string Provider { get; set; } 12 | 13 | /// 14 | /// 姓名 15 | /// 16 | public string Name { get; set; } 17 | 18 | /// 19 | /// 封面 20 | /// 21 | public string Cover { get; set; } 22 | 23 | /// 24 | /// 地址 25 | /// 26 | public string Url { get; set; } 27 | 28 | /// 29 | /// 图像类型 30 | /// 31 | public ImageType? ImageType { get; set; } 32 | 33 | /// 34 | /// 样品图片 35 | /// 36 | public List Samples { get; set; } 37 | 38 | /// 39 | /// 转换为字符串 40 | /// 41 | /// 42 | public override string ToString() 43 | => $"{Name}"; 44 | } 45 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/JavVideo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace Emby.Plugins.JavScraper.Scrapers 8 | { 9 | /// 10 | /// 视频 11 | /// 12 | public class JavVideo : JavVideoIndex 13 | { 14 | /// 15 | /// 原始标题 16 | /// 17 | private string _originalTitle; 18 | 19 | /// 20 | /// 原始标题 21 | /// 22 | public string OriginalTitle { get => string.IsNullOrWhiteSpace(_originalTitle) ? (_originalTitle = Title) : _originalTitle; set => _originalTitle = value; } 23 | 24 | /// 25 | /// 内容简介 26 | /// 27 | public string Plot { get; set; } 28 | 29 | /// 30 | /// 导演 31 | /// 32 | public string Director { get; set; } 33 | 34 | /// 35 | /// 影片时长 36 | /// 37 | public string Runtime { get; set; } 38 | 39 | /// 40 | /// 制作组 41 | /// 42 | public string Studio { get; set; } 43 | 44 | /// 45 | /// 厂商 46 | /// 47 | public string Maker { get; set; } 48 | 49 | /// 50 | /// 合集 51 | /// 52 | public string Set { get; set; } 53 | 54 | /// 55 | /// 类别 56 | /// 57 | public List Genres { get; set; } 58 | 59 | /// 60 | /// 演员 61 | /// 62 | public List Actors { get; set; } 63 | 64 | /// 65 | /// 样品图片 66 | /// 67 | public List Samples { get; set; } 68 | 69 | /// 70 | /// 公众评分 0-10之间。 71 | /// 72 | public float? CommunityRating { get; set; } 73 | 74 | /// 75 | /// %genre:中文字幕?中文:% 76 | /// 77 | private static Regex regex_genre = new Regex("%genre:(?[^?]+)?(?[^:]*):(?[^%]*)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); 78 | 79 | /// 80 | /// 获取格式化文件名 81 | /// 82 | /// 格式化字符串 83 | /// 空参数替代 84 | /// 是否移除路径中的非法字符 85 | /// 86 | public string GetFormatName(string format, string empty, bool clear_invalid_path_chars = false) 87 | { 88 | if (empty == null) 89 | empty = string.Empty; 90 | 91 | var m = this; 92 | void Replace(string key, string value) 93 | { 94 | var _index = format.IndexOf(key, StringComparison.OrdinalIgnoreCase); 95 | if (_index < 0) 96 | return; 97 | 98 | if (string.IsNullOrEmpty(value)) 99 | value = empty; 100 | 101 | do 102 | { 103 | format = format.Remove(_index, key.Length); 104 | format = format.Insert(_index, value); 105 | _index = format.IndexOf(key, _index + value.Length, StringComparison.OrdinalIgnoreCase); 106 | } while (_index >= 0); 107 | } 108 | 109 | Replace("%num%", m.Num); 110 | Replace("%title%", m.Title); 111 | Replace("%title_original%", m.OriginalTitle); 112 | Replace("%actor%", m.Actors?.Any() == true ? string.Join(", ", m.Actors) : null); 113 | Replace("%actor_first%", m.Actors?.FirstOrDefault()); 114 | Replace("%set%", m.Set); 115 | Replace("%director%", m.Director); 116 | Replace("%date%", m.Date); 117 | Replace("%year%", m.GetYear()?.ToString()); 118 | Replace("%month%", m.GetMonth()?.ToString("00")); 119 | Replace("%studio%", m.Studio); 120 | Replace("%maker%", m.Maker); 121 | 122 | do 123 | { 124 | //%genre:中文字幕?中文:% 125 | var match = regex_genre.Match(format); 126 | if (match.Success == false) 127 | break; 128 | var a = match.Groups["a"].Value; 129 | var genre_key = m.Genres?.Contains(a, StringComparer.OrdinalIgnoreCase) == true ? "b" : "c"; 130 | var genre_value = match.Groups[genre_key].Value; 131 | format = format.Replace(match.Value, genre_value); 132 | } while (true); 133 | 134 | //移除非法字符,以及修正路径分隔符 135 | if (clear_invalid_path_chars) 136 | { 137 | format = string.Join(" ", format.Split(Path.GetInvalidPathChars())); 138 | if (Path.DirectorySeparatorChar == '/') 139 | format = format.Replace('\\', '/'); 140 | else if (Path.DirectorySeparatorChar == '\\') 141 | format = format.Replace('/', '\\'); 142 | } 143 | 144 | return format; 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/JavVideoIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace Emby.Plugins.JavScraper.Scrapers 5 | { 6 | /// 7 | /// 视频索引 8 | /// 9 | public class JavVideoIndex 10 | { 11 | /// 12 | /// 适配器 13 | /// 14 | public string Provider { get; set; } 15 | 16 | /// 17 | /// 地址 18 | /// 19 | public string Url { get; set; } 20 | 21 | /// 22 | /// 番号 23 | /// 24 | public string Num { get; set; } 25 | 26 | /// 27 | /// 标题 28 | /// 29 | public string Title { get; set; } 30 | 31 | /// 32 | /// 封面 33 | /// 34 | public string Cover { get; set; } 35 | 36 | /// 37 | /// 日期 38 | /// 39 | public string Date { get; set; } 40 | 41 | /// 42 | /// 转换为字符串 43 | /// 44 | /// 45 | public override string ToString() 46 | => $"{Num} {Title}"; 47 | 48 | /// 49 | /// 获取年份 50 | /// 51 | /// 52 | public int? GetYear() 53 | { 54 | if (!(Date?.Length >= 4)) 55 | return null; 56 | if (int.TryParse(Date.Substring(0, 4), out var y) && y > 0) 57 | return y; 58 | return null; 59 | } 60 | 61 | /// 62 | /// 获取月份 63 | /// 64 | /// 65 | public int? GetMonth() 66 | { 67 | if (!(Date?.Length >= 6)) 68 | return null; 69 | var d = Date.Split("-/ 年月日".ToCharArray()); 70 | if (d.Length > 1) 71 | { 72 | if (int.TryParse(d[1], out var m) && m > 0 && m <= 12) 73 | return m; 74 | return null; 75 | } 76 | if (int.TryParse(Date.Substring(4, 2), out var m2) && m2 > 0 && m2 <= 12) 77 | return m2; 78 | return null; 79 | } 80 | 81 | #if __JELLYFIN__ 82 | /// 83 | /// 获取日期 84 | /// 85 | /// 86 | public DateTime? GetDate() 87 | { 88 | if (string.IsNullOrEmpty(Date)) 89 | return null; 90 | if (DateTime.TryParseExact(Date, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out DateTime result6)) 91 | { 92 | return result6.ToUniversalTime(); 93 | } 94 | else if (DateTime.TryParse(Date, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result6)) 95 | { 96 | return result6.ToUniversalTime(); 97 | } 98 | return null; 99 | } 100 | #else 101 | 102 | /// 103 | /// 获取日期 104 | /// 105 | /// 106 | public DateTimeOffset? GetDate() 107 | { 108 | if (string.IsNullOrEmpty(Date)) 109 | return null; 110 | if (DateTimeOffset.TryParseExact(Date, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out DateTimeOffset result6)) 111 | { 112 | return result6.ToUniversalTime(); 113 | } 114 | else if (DateTimeOffset.TryParse(Date, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result6)) 115 | { 116 | return result6.ToUniversalTime(); 117 | } 118 | return null; 119 | } 120 | 121 | #endif 122 | } 123 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/LevenshteinDistance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Emby.Plugins.JavScraper.Scrapers 4 | { 5 | /// 6 | /// 相似度计算 7 | /// 8 | public static class LevenshteinDistance 9 | { 10 | /// 11 | /// Calculate the difference between 2 strings using the Levenshtein distance algorithm 12 | /// 13 | /// First string 14 | /// Second string 15 | /// 16 | public static int Calculate(string source1, string source2) //O(n*m) 17 | { 18 | var source1Length = source1.Length; 19 | var source2Length = source2.Length; 20 | 21 | var matrix = new int[source1Length + 1, source2Length + 1]; 22 | 23 | // First calculation, if one entry is empty return full length 24 | if (source1Length == 0) 25 | return source2Length; 26 | 27 | if (source2Length == 0) 28 | return source1Length; 29 | 30 | // Initialization of matrix with row size source1Length and columns size source2Length 31 | for (var i = 0; i <= source1Length; matrix[i, 0] = i++) { } 32 | for (var j = 0; j <= source2Length; matrix[0, j] = j++) { } 33 | 34 | // Calculate rows and collumns distances 35 | for (var i = 1; i <= source1Length; i++) 36 | { 37 | for (var j = 1; j <= source2Length; j++) 38 | { 39 | var cost = (source2[j - 1] == source1[i - 1]) ? 0 : 1; 40 | 41 | matrix[i, j] = Math.Min( 42 | Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1), 43 | matrix[i - 1, j - 1] + cost); 44 | } 45 | } 46 | // return result 47 | return matrix[source1Length, source2Length]; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/MgsTage.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using HtmlAgilityPack; 3 | using MediaBrowser.Common.Net; 4 | #if __JELLYFIN__ 5 | using Microsoft.Extensions.Logging; 6 | #else 7 | using MediaBrowser.Model.Logging; 8 | #endif 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Net.Http; 13 | using System.Text.RegularExpressions; 14 | using System.Threading.Tasks; 15 | 16 | namespace Emby.Plugins.JavScraper.Scrapers 17 | { 18 | /// 19 | /// https://www.mgstage.com/product/product_detail/320MMGH-242/ 20 | /// 21 | public class MgsTage : AbstractScraper 22 | { 23 | /// 24 | /// 适配器名称 25 | /// 26 | public override string Name => "MgsTage"; 27 | 28 | private static Regex regexDate = new Regex(@"(?[\d]{4}[-/][\d]{2}[-/][\d]{2})", RegexOptions.Compiled | RegexOptions.IgnoreCase); 29 | 30 | /// 31 | /// 构造 32 | /// 33 | /// 34 | public MgsTage( 35 | #if __JELLYFIN__ 36 | ILoggerFactory logManager 37 | #else 38 | ILogManager logManager 39 | #endif 40 | ) 41 | : base("https://www.mgstage.com/", logManager.CreateLogger()) 42 | { 43 | } 44 | 45 | /// 46 | /// 检查关键字是否符合 47 | /// 48 | /// 49 | /// 50 | public override bool CheckKey(string key) 51 | => JavIdRecognizer.FC2(key) == null; 52 | 53 | /// 54 | /// 获取列表 55 | /// 56 | /// 关键字 57 | /// 58 | protected override async Task> DoQyery(List ls, string key) 59 | { 60 | //https://www.mgstage.com/search/search.php?search_word=320MMGH-242&disp_type=detail 61 | var doc = await GetHtmlDocumentAsync($"/search/search.php?search_word={key}&disp_type=detail"); 62 | if (doc != null) 63 | { 64 | ParseIndex(ls, doc); 65 | } 66 | 67 | SortIndex(key, ls); 68 | return ls; 69 | } 70 | 71 | /// 72 | /// 解析列表 73 | /// 74 | /// 75 | /// 76 | /// 77 | protected override List ParseIndex(List ls, HtmlDocument doc) 78 | { 79 | if (doc == null) 80 | return ls; 81 | var nodes = doc.DocumentNode.SelectNodes("//div[@class='rank_list']/ul/li"); 82 | if (nodes?.Any() != true) 83 | return ls; 84 | 85 | foreach (var node in nodes) 86 | { 87 | var title_node = node.SelectSingleNode("./h5/a"); 88 | if (title_node == null) 89 | continue; 90 | var url = title_node.GetAttributeValue("href", null); 91 | if (string.IsNullOrWhiteSpace(url)) 92 | continue; 93 | 94 | var m = new JavVideoIndex() 95 | { 96 | Provider = Name, 97 | Url = new Uri(client.BaseAddress, url).ToString(), 98 | Num = url.Split("/".ToArray(), StringSplitOptions.RemoveEmptyEntries).Last(), 99 | Title = title_node.InnerText.Trim() 100 | }; 101 | ls.Add(m); 102 | 103 | var img = node.SelectSingleNode("./h6/a/img"); 104 | if (img != null) 105 | { 106 | m.Cover = img.GetAttributeValue("src", null); 107 | } 108 | var date = node.SelectSingleNode(".//p[@class='data']"); 109 | if (date != null) 110 | { 111 | var d = date.InnerText.Trim(); 112 | var me = regexDate.Match(d); 113 | if (me.Success) 114 | m.Date = me.Groups["date"].Value.Replace('/', '-'); 115 | } 116 | } 117 | 118 | return ls; 119 | } 120 | 121 | /// 122 | /// 获取详情 123 | /// 124 | /// 地址 125 | /// 126 | public override async Task Get(string url) 127 | { 128 | //https://www.mgstage.com/product/product_detail/320MMGH-242/ 129 | var doc = await GetHtmlDocumentAsync(url); 130 | if (doc == null) 131 | return null; 132 | 133 | var node = doc.DocumentNode.SelectSingleNode("//div[@class='common_detail_cover']"); 134 | if (node == null) 135 | return null; 136 | 137 | var dic = new Dictionary(); 138 | var nodes = node.SelectNodes(".//table/tr/th/.."); 139 | foreach (var n in nodes) 140 | { 141 | var name = n.SelectSingleNode("./th")?.InnerText?.Trim(); 142 | if (string.IsNullOrWhiteSpace(name)) 143 | continue; 144 | //尝试获取 a 标签的内容 145 | var aa = n.SelectNodes("./td/a"); 146 | var value = aa?.Any() == true ? string.Join(", ", aa.Select(o => o.InnerText.Trim()).Where(o => string.IsNullOrWhiteSpace(o) == false)) 147 | : n.SelectSingleNode("./td")?.InnerText?.Trim(); 148 | 149 | if (string.IsNullOrWhiteSpace(value) == false) 150 | dic[name] = value; 151 | } 152 | 153 | string GetValue(string _key) 154 | => dic.Where(o => o.Key.Contains(_key)).Select(o => o.Value).FirstOrDefault(); 155 | 156 | var genres = GetValue("ジャンル")?.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries).ToList(); 157 | 158 | var actors = GetValue("出演")?.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries).ToList(); 159 | 160 | var samples = node.SelectNodes("//a[@class='sample_image']")? 161 | .Select(o => o.GetAttributeValue("href", null)).Where(o => o != null).ToList(); 162 | var m = new JavVideo() 163 | { 164 | Provider = Name, 165 | Url = url, 166 | Title = node.SelectSingleNode("./h1")?.InnerText?.Trim(), 167 | Cover = node.SelectSingleNode(".//img[@class='enlarge_image']")?.GetAttributeValue("src", null), 168 | Num = GetValue("品番"), 169 | Date = GetValue("配信開始日")?.Replace('/', '-'), 170 | Runtime = GetValue("収録時間"), 171 | Maker = GetValue("發行商"), 172 | Studio = GetValue("メーカー"), 173 | Set = GetValue("シリーズ"), 174 | Director = GetValue("シリーズ"), 175 | Plot = node.SelectSingleNode("//p[@class='txt introduction']")?.InnerText, 176 | Genres = genres, 177 | Actors = actors, 178 | Samples = samples, 179 | }; 180 | 181 | if (string.IsNullOrWhiteSpace(m.Plot)) 182 | m.Plot = await GetDmmPlot(m.Num); 183 | 184 | //去除标题中的番号 185 | if (string.IsNullOrWhiteSpace(m.Num) == false && m.Title?.StartsWith(m.Num, StringComparison.OrdinalIgnoreCase) == true) 186 | m.Title = m.Title.Substring(m.Num.Length).Trim(); 187 | 188 | return m; 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Scrapers/R18.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | #if __JELLYFIN__ 4 | using Microsoft.Extensions.Logging; 5 | #else 6 | 7 | using MediaBrowser.Model.Logging; 8 | 9 | #endif 10 | 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Text.RegularExpressions; 15 | using System.Threading.Tasks; 16 | using System.Web; 17 | 18 | namespace Emby.Plugins.JavScraper.Scrapers 19 | { 20 | /// 21 | /// https://www.r18.com/videos/vod/movies/detail/-/id=118abw00032/?i3_ref=search&i3_ord=1 22 | /// 23 | public class R18 : AbstractScraper 24 | { 25 | /// 26 | /// 适配器名称 27 | /// 28 | public override string Name => "R18"; 29 | 30 | /// 31 | /// 构造 32 | /// 33 | /// 34 | public R18( 35 | #if __JELLYFIN__ 36 | ILoggerFactory logManager 37 | #else 38 | ILogManager logManager 39 | #endif 40 | ) 41 | : base("https://www.r18.com/", logManager.CreateLogger()) 42 | { 43 | } 44 | 45 | /// 46 | /// 检查关键字是否符合 47 | /// 48 | /// 49 | /// 50 | public override bool CheckKey(string key) 51 | => JavIdRecognizer.FC2(key) == null; 52 | 53 | /// 54 | /// 获取列表 55 | /// 56 | /// 关键字 57 | /// 58 | protected override async Task> DoQyery(List ls, string key) 59 | { 60 | //https://www.r18.com/common/search/searchword=ABW-032/ 61 | var doc = await GetHtmlDocumentAsync($"/common/search/searchword={key}/?lg=zh"); 62 | if (doc != null) 63 | { 64 | ParseIndex(ls, doc); 65 | } 66 | 67 | SortIndex(key, ls); 68 | return ls; 69 | } 70 | 71 | /// 72 | /// 解析列表 73 | /// 74 | /// 75 | /// 76 | /// 77 | protected override List ParseIndex(List ls, HtmlDocument doc) 78 | { 79 | if (doc == null) 80 | return ls; 81 | var nodes = doc.DocumentNode.SelectNodes("//li[@class='item-list']"); 82 | if (nodes?.Any() != true) 83 | return ls; 84 | 85 | foreach (var node in nodes) 86 | { 87 | var title_node = node.SelectSingleNode("./a"); 88 | if (title_node == null) 89 | continue; 90 | var url = title_node.GetAttributeValue("href", null); 91 | if (string.IsNullOrWhiteSpace(url)) 92 | continue; 93 | var img = title_node.SelectSingleNode(".//img"); 94 | if (img == null) 95 | continue; 96 | var t2 = title_node.SelectSingleNode(".//dt"); 97 | var m = new JavVideoIndex() 98 | { 99 | Provider = Name, 100 | Url = url + "&lg=zh", 101 | Num = img.GetAttributeValue("alt", null), 102 | Title = t2?.InnerText.Trim(), 103 | Cover = img.GetAttributeValue("src", null), 104 | }; 105 | if (string.IsNullOrWhiteSpace(m.Title)) 106 | m.Title = m.Num; 107 | 108 | ls.Add(m); 109 | } 110 | 111 | return ls; 112 | } 113 | 114 | /// 115 | /// 获取详情 116 | /// 117 | /// 地址 118 | /// 119 | public override async Task Get(string url) 120 | { 121 | //https://www.r18.com/videos/vod/movies/detail/-/id=ssni00879/?dmmref=video.movies.popular&i3_ref=list&i3_ord=4 122 | var doc = await GetHtmlDocumentAsync(url); 123 | if (doc == null) 124 | return null; 125 | 126 | var node = doc.DocumentNode.SelectSingleNode("//div[@class='product-details-page']"); 127 | if (node == null) 128 | return null; 129 | 130 | var product_details = node.SelectSingleNode(".//div[@class='product-details']"); 131 | 132 | string GetValueByItemprop(string name) 133 | => product_details.SelectSingleNode($".//dd[@itemprop='{name}']")?.InnerText.Trim().Trim('-'); 134 | 135 | string GetDuration() 136 | { 137 | var _d = GetValueByItemprop("duration"); 138 | if (string.IsNullOrWhiteSpace(_d)) 139 | return null; 140 | var _m = Regex.Match(_d, @"[\d]+"); 141 | if (_m.Success) 142 | return _m.Value; 143 | return null; 144 | } 145 | var dic = new Dictionary(); 146 | var nodes = product_details.SelectNodes(".//dt"); 147 | foreach (var n in nodes) 148 | { 149 | var name = n.InnerText.Trim(); 150 | if (string.IsNullOrWhiteSpace(name)) 151 | continue; 152 | //获取下一个标签 153 | var nx = n; 154 | do 155 | { 156 | nx = nx.NextSibling; 157 | if (nx == null || nx.Name == "dt") 158 | { 159 | nx = null; 160 | break; 161 | } 162 | if (nx.Name == "dd") 163 | break; 164 | } while (true); 165 | if (nx == null) 166 | continue; 167 | 168 | var aa = nx.SelectNodes(".//a"); 169 | var value = aa?.Any() == true ? string.Join(", ", aa.Select(o => o.InnerText.Trim()?.Trim('-')).Where(o => string.IsNullOrWhiteSpace(o) == false)) 170 | : nx?.InnerText?.Trim()?.Trim('-'); 171 | 172 | if (string.IsNullOrWhiteSpace(value) == false) 173 | dic[name] = value; 174 | } 175 | 176 | string GetValue(string _key) 177 | => dic.Where(o => o.Key.Contains(_key)).Select(o => o.Value).FirstOrDefault(); 178 | 179 | var genres = product_details.SelectNodes(".//*[@itemprop='genre']") 180 | .Select(o => o.InnerText.Trim()?.Trim('-')).Where(o => string.IsNullOrWhiteSpace(o) == false).ToList(); 181 | 182 | var actors = product_details.SelectNodes(".//div[@itemprop='actors']//*[@itemprop='name']") 183 | .Select(o => o.InnerText.Trim()?.Trim('-')).Where(o => string.IsNullOrWhiteSpace(o) == false).ToList(); 184 | 185 | var product_gallery = doc.GetElementbyId("product-gallery"); 186 | var samples = product_gallery.SelectNodes(".//img")? 187 | .Select(o => o.GetAttributeValue("data-src", null) ?? o.GetAttributeValue("src", null)).Where(o => o != null).ToList(); 188 | 189 | var m = new JavVideo() 190 | { 191 | Provider = Name, 192 | Url = url, 193 | Title = HttpUtility.HtmlDecode(node.SelectSingleNode(".//cite")?.InnerText?.Trim() ?? string.Empty), 194 | Cover = node.SelectSingleNode(".//img[@itemprop='image']")?.GetAttributeValue("src", null)?.Replace("ps.", "pl."), 195 | Num = GetValue("DVD ID:"), 196 | Date = GetValueByItemprop("dateCreated")?.Replace('/', '-'), 197 | Runtime = GetDuration(), 198 | Maker = GetValue("片商:"), 199 | Studio = GetValue("廠牌:"), 200 | Set = GetValue("系列:"), 201 | Director = GetValueByItemprop("director"), 202 | //Plot = node.SelectSingleNode("//p[@class='txt introduction']")?.InnerText, 203 | Genres = genres, 204 | Actors = actors, 205 | Samples = samples, 206 | }; 207 | 208 | if (string.IsNullOrWhiteSpace(m.Title)) 209 | m.Title = m.Num; 210 | 211 | if (string.IsNullOrWhiteSpace(m.Plot)) 212 | m.Plot = await GetDmmPlot(m.Num); 213 | 214 | //去除标题中的番号 215 | if (string.IsNullOrWhiteSpace(m.Num) == false && m.Title?.StartsWith(m.Num, StringComparison.OrdinalIgnoreCase) == true) 216 | m.Title = m.Title.Substring(m.Num.Length).Trim(); 217 | 218 | return m; 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Services/ImageService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using MediaBrowser.Common.Extensions; 3 | using MediaBrowser.Controller.Net; 4 | using MediaBrowser.Model.Entities; 5 | using MediaBrowser.Model.Services; 6 | 7 | #if __JELLYFIN__ 8 | using Microsoft.Extensions.Logging; 9 | #else 10 | using MediaBrowser.Model.Logging; 11 | #endif 12 | 13 | namespace Emby.Plugins.JavScraper.Services 14 | { 15 | /// 16 | /// 转发图片信息 17 | /// 18 | [Route("/emby/Plugins/JavScraper/Image", "GET")] 19 | public class GetImageInfo 20 | { 21 | /// 22 | /// 图像类型 23 | /// 24 | public ImageType? type { get; set; } 25 | 26 | /// 27 | /// 地址 28 | /// 29 | public string url { get; set; } 30 | } 31 | 32 | public class ImageService : IService, IRequiresRequest 33 | { 34 | private readonly ImageProxyService imageProxyService; 35 | private readonly IHttpResultFactory resultFactory; 36 | private readonly ILogger logger; 37 | 38 | /// 39 | /// Gets or sets the request context. 40 | /// 41 | /// The request context. 42 | public IRequest Request { get; set; } 43 | 44 | public ImageService( 45 | #if __JELLYFIN__ 46 | ILoggerFactory logManager, 47 | #else 48 | ILogManager logManager, 49 | ImageProxyService imageProxyService, 50 | #endif 51 | IHttpResultFactory resultFactory 52 | ) 53 | { 54 | #if __JELLYFIN__ 55 | imageProxyService = Plugin.Instance.ImageProxyService; 56 | #else 57 | this.imageProxyService = imageProxyService; 58 | #endif 59 | this.resultFactory = resultFactory; 60 | this.logger = logManager.CreateLogger(); 61 | } 62 | 63 | public object Get(GetImageInfo request) 64 | => DoGet(request?.url, request?.type); 65 | 66 | /// 67 | /// 转发信息 68 | /// 69 | /// 70 | /// 71 | private async Task DoGet(string url, ImageType? type) 72 | { 73 | logger.Info($"{url}"); 74 | 75 | if (url.IsWebUrl() != true) 76 | throw new ResourceNotFoundException(); 77 | 78 | var resp = await imageProxyService.GetImageResponse(url, type ?? ImageType.Backdrop, default); 79 | if (!(resp?.ContentLength > 0)) 80 | throw new ResourceNotFoundException(); 81 | 82 | return resultFactory.GetResult(Request, resp.Content, resp.ContentType); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Services/TranslationService.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Baidu; 2 | using Emby.Plugins.JavScraper.Data; 3 | #if __JELLYFIN__ 4 | using Microsoft.Extensions.Logging; 5 | #else 6 | using MediaBrowser.Model.Logging; 7 | #endif 8 | using MediaBrowser.Model.Serialization; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace Emby.Plugins.JavScraper.Services 15 | { 16 | /// 17 | /// 翻译服务 18 | /// 19 | public class TranslationService 20 | { 21 | private readonly IJsonSerializer _jsonSerializer; 22 | private readonly ILogger _logger; 23 | private static NamedLockerAsync _locker = new NamedLockerAsync(); 24 | 25 | /// 26 | /// 翻译 27 | /// 28 | /// 29 | public TranslationService(IJsonSerializer jsonSerializer, ILogger logger) 30 | { 31 | _jsonSerializer = jsonSerializer; 32 | _logger = logger; 33 | } 34 | 35 | /// 36 | /// 翻译 37 | /// 38 | /// 39 | /// 40 | public async Task Fanyi(string src) 41 | { 42 | if (string.IsNullOrWhiteSpace(src)) 43 | return src; 44 | 45 | var lang = Plugin.Instance.Configuration.BaiduFanyiLanguage?.Trim(); 46 | if (string.IsNullOrWhiteSpace(lang)) 47 | lang = "zh"; 48 | 49 | var hash = Translation.CalcHash(src); 50 | 51 | using (await _locker.LockAsync(hash)) 52 | { 53 | try 54 | { 55 | var item = Plugin.Instance.db.Translations.FindOne(o => o.hash == hash && o.lang == lang); 56 | if (item != null) 57 | return item.dst; 58 | 59 | var fanyi_result = await BaiduFanyiService.Fanyi(src, _jsonSerializer); 60 | if (fanyi_result?.trans_result?.Any() == true) 61 | { 62 | var dst = string.Join("\n", fanyi_result.trans_result.Select(o => o.dst)); 63 | if (string.IsNullOrWhiteSpace(dst) == false) 64 | { 65 | item = new Translation() 66 | { 67 | hash = hash, 68 | lang = lang, 69 | src = src, 70 | dst = dst, 71 | created = DateTime.Now, 72 | modified = DateTime.Now, 73 | }; 74 | Plugin.Instance.db.Translations.Insert(item); 75 | return dst; 76 | } 77 | } 78 | 79 | return src; 80 | } 81 | catch (Exception ex) 82 | { 83 | _logger.Error($"{src} {ex.Message}"); 84 | } 85 | } 86 | 87 | return src; 88 | } 89 | 90 | /// 91 | /// 翻译 92 | /// 93 | /// 94 | /// 95 | public async Task> Fanyi(List values) 96 | { 97 | if (values?.Any() != true) 98 | return values; 99 | 100 | var ls = new List(); 101 | 102 | foreach (var src in values) 103 | ls.Add(await Fanyi(src)); 104 | 105 | return ls; 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/Services/UpdateService.cs: -------------------------------------------------------------------------------- 1 | using Emby.Plugins.JavScraper.Http; 2 | using Emby.Plugins.JavScraper.Scrapers; 3 | using MediaBrowser.Common.Configuration; 4 | using MediaBrowser.Common.Net; 5 | using MediaBrowser.Model.IO; 6 | 7 | #if __JELLYFIN__ 8 | using Microsoft.Extensions.Logging; 9 | #else 10 | using MediaBrowser.Model.Logging; 11 | #endif 12 | 13 | using MediaBrowser.Model.Serialization; 14 | using MediaBrowser.Model.Services; 15 | using System; 16 | using System.Diagnostics; 17 | using System.IO; 18 | using System.Linq; 19 | using System.Net.Http; 20 | using System.Reflection; 21 | using System.Text.RegularExpressions; 22 | using System.Threading.Tasks; 23 | 24 | namespace Emby.Plugins.JavScraper.Services 25 | { 26 | /// 27 | /// 更新信息 28 | /// 29 | [Route("/emby/Plugins/JavScraper/Update", "GET")] 30 | public class GetUpdateInfo : IReturn 31 | { 32 | /// 33 | /// 是否更新 34 | /// 35 | public bool update { get; set; } 36 | } 37 | 38 | public class UpdateService : IService 39 | { 40 | private readonly IFileSystem fileSystem; 41 | private readonly IHttpClient httpClient; 42 | private readonly IZipClient zipClient; 43 | private readonly IJsonSerializer jsonSerializer; 44 | private readonly IApplicationPaths appPaths; 45 | private readonly ILogger logger; 46 | private static Regex regexVersion = new Regex(@"\d+(?:\.\d+)+"); 47 | private HttpClientEx client; 48 | 49 | public UpdateService(IFileSystem fileSystem, IHttpClient httpClient, IZipClient zipClient, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, 50 | #if __JELLYFIN__ 51 | ILoggerFactory logManager 52 | #else 53 | ILogManager logManager 54 | #endif 55 | ) 56 | { 57 | this.fileSystem = fileSystem; 58 | this.httpClient = httpClient; 59 | this.zipClient = zipClient; 60 | this.jsonSerializer = jsonSerializer; 61 | this.appPaths = appPaths; 62 | this.logger = logManager.CreateLogger(); 63 | client = new HttpClientEx(client => client.DefaultRequestHeaders.UserAgent.TryParseAdd($"JavScraper v{Assembly.GetExecutingAssembly().GetName().Version}")); 64 | } 65 | 66 | public object Get(GetUpdateInfo request) 67 | { 68 | return Task.Run(() => Do(request)).GetAwaiter().GetResult(); 69 | } 70 | 71 | private async Task Do(GetUpdateInfo request) 72 | { 73 | var r = new UpdateInfoData() 74 | { 75 | LoadedVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(), 76 | PendingLoadVersion = GetPendingLoadVersion(), 77 | }; 78 | try 79 | { 80 | var resp = await client.GetAsync("https://api.github.com/repos/JavScraper/Emby.Plugins.JavScraper/releases/latest"); 81 | 82 | if (resp.StatusCode == System.Net.HttpStatusCode.OK) 83 | { 84 | var data = jsonSerializer.DeserializeFromStream(await resp.Content.ReadAsStreamAsync()); 85 | r.UpdateMessage = data.body; 86 | 87 | string key = 88 | #if __JELLYFIN__ 89 | "Jellyfin"; 90 | #else 91 | "Emby.JavScraper"; 92 | #endif 93 | 94 | foreach (var v in data.assets.Where(o => o.name.IndexOf(key, StringComparison.OrdinalIgnoreCase) >= 0 && o.name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))) 95 | { 96 | var m = regexVersion.Match(v.name); 97 | if (m.Success) 98 | { 99 | r.LatestVersion = m.ToString(); 100 | r.LatestUrl = v.browser_download_url; 101 | break; 102 | } 103 | } 104 | } 105 | else 106 | { 107 | r.ErrorMessage = "获取新版下失败。"; 108 | return r; 109 | } 110 | } 111 | catch (Exception ex) 112 | { 113 | r.ErrorMessage = ex.Message; 114 | return r; 115 | } 116 | 117 | if (request.update == true && r.HasNewVersion) 118 | { 119 | try 120 | { 121 | var ms = await client.GetStreamAsync(r.LatestUrl); 122 | zipClient.ExtractAllFromZip(ms, appPaths.PluginsPath, true); 123 | r.PendingLoadVersion = GetPendingLoadVersion(); 124 | } 125 | catch (Exception ex) 126 | { 127 | r.ErrorMessage = $"更新失败:{ex.Message}"; 128 | } 129 | } 130 | 131 | //r.PendingLoadVersion = "1.0.0"; 132 | //r.LoadedVersion = "1.0.0"; 133 | return r; 134 | } 135 | 136 | private string GetPendingLoadVersion() 137 | { 138 | var file = Path.Combine(appPaths.PluginsPath, "JavScraper.dll"); 139 | if (File.Exists(file) == false) 140 | return null; 141 | return FileVersionInfo.GetVersionInfo(file)?.FileVersion; 142 | } 143 | } 144 | 145 | /// 146 | /// 更新信息 147 | /// 148 | public class UpdateInfoData 149 | { 150 | /// 151 | /// 服务器上的版本 152 | /// 153 | public string LatestVersion { get; set; } 154 | 155 | /// 156 | /// 下载地址 157 | /// 158 | public string LatestUrl { get; set; } 159 | 160 | /// 161 | /// 加载中的版本 162 | /// 163 | public string LoadedVersion { get; set; } 164 | 165 | /// 166 | /// 待加载版本 167 | /// 168 | public string PendingLoadVersion { get; set; } 169 | 170 | /// 171 | /// 更新信息 172 | /// 173 | public string UpdateMessage { get; set; } 174 | 175 | /// 176 | /// 是否包含新版本 177 | /// 178 | public bool HasNewVersion 179 | { 180 | get 181 | { 182 | try 183 | { 184 | return string.IsNullOrWhiteSpace(LatestVersion) == false && new Version(LatestVersion) > new Version(PendingLoadVersion ?? "0.0.0.1"); 185 | } 186 | catch { } 187 | 188 | return false; 189 | } 190 | } 191 | 192 | public string ErrorMessage { get; set; } 193 | 194 | public bool Success => string.IsNullOrEmpty(ErrorMessage); 195 | 196 | /// 197 | /// 0 错误 198 | /// 1 新版本 199 | /// 2 需要重启 200 | /// 3 最新 201 | /// 202 | public int State 203 | { 204 | get 205 | { 206 | if (string.IsNullOrWhiteSpace(ErrorMessage) == false) 207 | return 0; 208 | if (HasNewVersion) 209 | return 1; 210 | if (string.IsNullOrWhiteSpace(LatestVersion) == false && new Version(LatestVersion) > new Version(LoadedVersion ?? "0.0.0.1")) 211 | return 2; 212 | return 3; 213 | } 214 | } 215 | } 216 | 217 | public class Rootobject 218 | { 219 | public string url { get; set; } 220 | public string tag_name { get; set; } 221 | public string name { get; set; } 222 | public DateTime created_at { get; set; } 223 | public DateTime published_at { get; set; } 224 | public Asset[] assets { get; set; } 225 | public string body { get; set; } 226 | } 227 | 228 | public class Asset 229 | { 230 | public string name { get; set; } 231 | public object label { get; set; } 232 | public string content_type { get; set; } 233 | public string state { get; set; } 234 | public int size { get; set; } 235 | 236 | /// 237 | /// 创建时间 238 | /// 239 | public DateTime created_at { get; set; } 240 | 241 | /// 242 | /// 更新时间 243 | /// 244 | public DateTime updated_at { get; set; } 245 | 246 | /// 247 | /// 下载地址 248 | /// 249 | public string browser_download_url { get; set; } 250 | } 251 | } -------------------------------------------------------------------------------- /Emby.Plugins.JavScraper/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavScraper/Emby.Plugins.JavScraper/7ef5fbfdb26b3ac61d59a32030da899bb871c029/Emby.Plugins.JavScraper/thumb.png -------------------------------------------------------------------------------- /Jellyfin.GenerateConfigurationPage/Jellyfin.GenerateConfigurationPage.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Jellyfin.GenerateConfigurationPage/Program.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace Jellyfin.GenerateConfigurationPage 8 | { 9 | internal class Program 10 | { 11 | private static void Main(string[] args) 12 | { 13 | var path = "../../../../Emby.Plugins.JavScraper/Configuration"; 14 | Do(path, "ConfigPage.html", "Jellyfin.ConfigPage.html"); 15 | Do(path, "JavOrganizationConfigPage.html", "Jellyfin.JavOrganizationConfigPage.html"); 16 | Console.WriteLine("Hello World!"); 17 | } 18 | 19 | private static HtmlNode selectArrowContainerNode = HtmlNode.CreateNode(@"
20 |
0
21 | 22 |
"); 23 | 24 | private static void Do(string path, string input, string output) 25 | { 26 | var en = Environment.CurrentDirectory; 27 | var file = Path.Combine(path, input); 28 | 29 | var doc = new HtmlDocument(); 30 | var html = File.ReadAllText(file); 31 | 32 | html = html.Replace(@"search", @""); 33 | 34 | html = Regex.Replace(html, "", @""); 35 | html = Regex.Replace(html, "", @""); 36 | 37 | doc.LoadHtml(html); 38 | 39 | //替换折叠的 40 | var nodes = doc.DocumentNode.SelectNodes("//div[@is='emby-collapse']"); 41 | if (nodes?.Any() == true) 42 | foreach (var node in nodes) 43 | { 44 | var title = node.GetAttributeValue("title", string.Empty); 45 | var body = node.SelectSingleNode("//div[@class='collapseContent']") ?? node; 46 | 47 | var nw = HtmlNode.CreateNode(@$"
48 |

{title}

49 |
"); 50 | nw.AppendChildren(body.ChildNodes); 51 | 52 | node.ParentNode.InsertBefore(nw, node); 53 | node.Remove(); 54 | } 55 | 56 | //替换下拉 57 | nodes = doc.DocumentNode.SelectNodes("//div[@class='selectArrowContainer']"); 58 | if (nodes?.Any() == true) 59 | foreach (var node in nodes) 60 | { 61 | node.ParentNode.InsertBefore(selectArrowContainerNode, node); 62 | node.Remove(); 63 | } 64 | 65 | file = Path.Combine(path, output); 66 | doc.Save(file); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emby.Plugins.JavScraper 2 | Emby/Jellyfin 的一个日本电影刮削器插件,可以从某些网站抓取影片信息。 3 | 4 | [https://javscraper.com](https://javscraper.com) 5 | 6 | ![Jav Scraper Logo](./Emby.Plugins.JavScraper/thumb.png) 7 | 8 | 关键字:**_Jav_**, **_Scraper_**, **_Jav Scraper_**, **_Emby Plugins_**, **_Jellyfin Plugins_**, **_JavBus_**, **_JavDB_**, **_FC2_**, **_Japanese_**, **_Adult_**, **_Movie_**, **_Metadata_**, **_刮削器_**, **_插件_**, **_日本_**, **_电影_**, **_元数据_**, **_番号_** 9 | 10 | # 目录 11 | - [主要原理](#主要原理) 12 | - [支持的采集来源](#支持的采集来源) 13 | - [如何使用](#如何使用) 14 | * [部署修改版 jsproxy](#部署修改版-jsproxy) 15 | * [插件安装](#插件安装) 16 | * [插件更新](#插件更新) 17 | * [配置](#配置) 18 | * [使用](#使用) 19 | * [女优头像](#女优头像) 20 | * [特别建议](#特别建议) 21 | - [计划新增特性](#计划新增特性) 22 | - [反馈](#反馈) 23 | - [截图](#截图) 24 | * [效果](#效果) 25 | + [媒体库](#媒体库) 26 | + [影片详情](#影片详情) 27 | + [识别](#识别) 28 | * [配置](#配置) 29 | + [Jav Scraper 配置](#jav-scraper-配置) 30 | + [媒体库配置](#媒体库配置) 31 | + [女优头像采集](#女优头像采集) 32 | 33 | # 主要原理 34 | - 通过在 [CloudFlare Worker](https://workers.cloudflare.com) 上架设的**修改版 [jsproxy](https://github.com/EtherDream/jsproxy)** 作为代理,用于访问几个网站下载元数据和图片。 35 | - 安装到 Emby 的 JavScraper 刮削器插件,根据文件名/文件夹名称找到番号,并下载元数据和图片。 36 | 37 | > 目前已经支持 HTTP/HTTPS/SOCKS5 代理方式。 38 | 39 | # 支持的采集来源 40 | - [JavBus](https://www.javbus.com/) 41 | - [JavDB](https://javdb.com/) 42 | - [MsgTage](https://www.mgstage.com/) 43 | - [FC2](https://fc2club.com/) 44 | - [AVSOX](https://avsox.host/) 45 | - [Jav123](https://www.jav321.com/) 46 | - [R18](https://www.r18.com/) 47 | 48 | # 如何使用 49 | 50 | ## 部署修改版 jsproxy 51 | 具体参见[使用 CloudFlare Worker 免费部署](cf-worker/README.md) 52 | > 默认已经配置了一个代理,多人使用会超过免费的额度,建议自己配置;非中国区或全局穿墙用户,可禁用该代理。 53 | 54 | > 目前已经支持 HTTP/HTTPS/SOCKS5 代理方式。 55 | 56 | ## 插件安装 57 | - [点击这里下载最新的插件文件](https://github.com/JavScraper/Emby.Plugins.JavScraper/releases),解压出里面的 **JavScraper.dll** 文件,通过ssh等方式拷贝到 Emby 的插件目录 58 | - 常见的插件目录如下: 59 | - 群晖 60 | - /volume1/Emby/plugins 61 | - /var/packages/EmbyServer/var/plugins 62 | - /volume1/@appdata/EmbyServer/plugins 63 | - Windows 64 | - emby\programdata\plugins 65 | - 需要**重启Emby服务**,插件才生效。 66 | 67 | ## 插件更新 68 | - 打开 **JavScraper** 配置页面的时,会自动检查更新(在页面的最下方)。 69 | - 如果有更新,则点击**立即更新**,并在**重启 Emby Server** 后生效。 70 | 71 | ## 配置 72 | - 在 **服务器** 配置菜单中找到 **Jav Scraper**,或者 **插件** 菜单中找到 **Jav Scraper** 。 73 | - 配置你自己的 jsproxy 地址 或者 HTTP/HTTPS/SOCKS5 代理。 74 | > 非中国区或全局穿墙用户,可禁用该代理。 75 | - 在**媒体库**中,找到你的**日本电影**的媒体库,并编辑: 76 | - 媒体库类型必须是**电影** 77 | - **显示高级设置** 78 | - 在 **Movie元数据下载器** 中只 勾选 **JavScraper** 79 | - 在 **Movie图片获取程序** 中只 勾选 **JavScraper** 80 | 81 | ## 使用 82 | - _添加新影片后_:在**媒体库**中点 **扫描媒体库文件**; 83 | - _如果需要更新全部元数据_:在**媒体库**中点 **刷新元数据** 84 | - _如果需要更新某影片元数据_:在**影片**中点 **识别** ,并输入番号查找。 85 | 86 | ## 女优头像 87 | 88 | ~~参见 [Emby 女优头像批量导入工具](Emby.Actress/README.md)。~~ 89 | 90 | 已经集成头像采集,可以在 **控制台-高级-计划任务** 中找到 **JavScraper: 采集缺失的女优头像**,并点击右边的三角符号开始启动采集任务。 91 | 92 | 头像数据源来自 [女友头像仓库](https://github.com/xinxin8816/gfriends) 93 | 94 | 95 | ## 特别建议 96 | - Emby 自动搜索元数据时,会将非根文件夹的名称作为关键字,所以,需要非根文件夹名称中包含番号信息。 97 | - 如果自动搜索元数据失败或者不正确时,请使用 **识别** 功能手动刷新 _单部影片_ 的元数据 或者 修改文件夹、文件名称后再 **扫描媒体库文件**。 98 | - 强烈建议配置**百度的人体分析**接口,这样封面生成会更加准确(_默认等比例截取右边部分作为封面_)。 99 | 100 | # 计划新增特性 101 | - [x] 支持某些域名不走代理 102 | - [x] 支持禁用代理 103 | - [x] 支持移除某些标签 104 | - [x] 标签从日文转为中文 105 | - [x] 翻译影片标题、标签、简介 106 | - [x] 刮削器支持排序 107 | - [x] 支持HTTP/HTTPS/SOCKS5代理 108 | - [x] 采集女优头像 109 | - [x] 刮削器支持重新指定网站的域名 110 | - [ ] 文件整理 111 | 112 | # 反馈 113 | 如果有什么想法,请在[提交反馈](https://github.com/JavScraper/Emby.Plugins.JavScraper/issues)。 114 | 115 | 116 | # 截图 117 | 118 | ## 效果 119 | 120 | ### 媒体库 121 | ![Movie Library](https://javscraper.com/Emby.Plugins/Screenshots/Screenshot02.png) 122 | 123 | ### 影片详情 124 | ![Movie Details](https://javscraper.com/Emby.Plugins/Screenshots/Screenshot03.png) 125 | 126 | ### 识别 127 | ![Movie Search](https://javscraper.com/Emby.Plugins/Screenshots/Screenshot04.png) 128 | 129 | ## 配置 130 | ### Jav Scraper 配置 131 | ![Jav Scraper Configuration](https://javscraper.com/Emby.Plugins/Screenshots/Screenshot01.png) 132 | 133 | ### 媒体库配置 134 | 135 | ![Library Edit](https://javscraper.com/Emby.Plugins/Screenshots/LibraryEdit01.png) 136 | ![Library Edit](https://javscraper.com/Emby.Plugins/Screenshots/LibraryEdit02.png) 137 | 138 | ### 女优头像采集 139 | ![Actress](https://javscraper.com/Emby.Plugins/Screenshots/Actress01.png) -------------------------------------------------------------------------------- /Screenshots/Screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavScraper/Emby.Plugins.JavScraper/7ef5fbfdb26b3ac61d59a32030da899bb871c029/Screenshots/Screenshot01.png -------------------------------------------------------------------------------- /cf-worker/README.md: -------------------------------------------------------------------------------- 1 | 使用 CloudFlare Worker 免费部署 2 | 3 | # 简介 4 | 5 | `CloudFlare Worker` 是 CloudFlare 的边缘计算服务。开发者可通过 JavaScript 对 CDN 进行编程,从而能灵活处理 HTTP 请求。这使得很多任务可在 CDN 上完成,无需自己的服务器参与。 6 | 7 | 8 | # 部署 9 | 10 | 首页:[https://workers.cloudflare.com](https://workers.cloudflare.com) 11 | 12 | 注册,登陆,`Start building`,取一个子域名,`Create a Worker`。 13 | 14 | 复制 [index.js](index.js) 到左侧代码框,`Save and deploy`。如果正常,右侧应显示首页。 15 | 16 | 收藏地址框中的 `https://xxxx.子域名.workers.dev`,以后可直接访问。 17 | 18 | 19 | # 计费 20 | 21 | 后退到 `overview` 页面可参看使用情况。免费版每天有 10 万次免费请求,对于个人通常足够。 22 | 23 | 24 | # 特别说明 25 | - 该文件是在 [jsproxy](https://github.com/EtherDream/jsproxy) 的基础进行是修改和简化,在此对原作者[EtherDream](https://github.com/EtherDream)表示感谢。 26 | - 移除了默认的首页,主要是避免用作其他 27 | - 去除请求校验 28 | -------------------------------------------------------------------------------- /cf-worker/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {RequestInit} */ 4 | const PREFLIGHT_INIT = { 5 | status: 204, 6 | headers: new Headers({ 7 | 'access-control-allow-origin': '*', 8 | 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', 9 | 'access-control-max-age': '1728000', 10 | }), 11 | } 12 | 13 | /** 14 | * @param {any} body 15 | * @param {number} status 16 | * @param {Object} headers 17 | */ 18 | function makeRes(body, status = 200, headers = {}) { 19 | headers['access-control-allow-origin'] = '*' 20 | return new Response(body, { status, headers }) 21 | } 22 | 23 | /** 24 | * @param {string} urlStr 25 | */ 26 | function newUrl(urlStr) { 27 | try { 28 | return new URL(urlStr) 29 | } catch (err) { 30 | return null 31 | } 32 | } 33 | 34 | addEventListener('fetch', e => { 35 | const ret = fetchHandler(e) 36 | .catch(err => makeRes('cfworker error:\n' + err.stack, 502)) 37 | e.respondWith(ret) 38 | }) 39 | 40 | /** 41 | * @param {FetchEvent} e 42 | */ 43 | async function fetchHandler(e) { 44 | const req = e.request 45 | const urlStr = req.url 46 | const urlObj = new URL(urlStr) 47 | const path = urlObj.href.substr(urlObj.origin.length) 48 | 49 | if (path.startsWith('/http/')) { 50 | return await httpHandler(req, path.substr(6)) 51 | } 52 | 53 | return makeRes('hello!') 54 | } 55 | 56 | /** 57 | * @param {Request} req 58 | * @param {string} pathname 59 | */ 60 | async function httpHandler(req, pathname) { 61 | const reqHdrRaw = req.headers 62 | if (reqHdrRaw.has('x-jsproxy')) { 63 | return Response.error() 64 | } 65 | 66 | // preflight 67 | if (req.method === 'OPTIONS' && reqHdrRaw.has('access-control-request-headers')) { 68 | return new Response(null, PREFLIGHT_INIT) 69 | } 70 | 71 | const reqHdrNew = new Headers(reqHdrRaw) 72 | reqHdrNew.set('x-jsproxy', '1') 73 | 74 | if (!reqHdrRaw.has('user-agent')) { 75 | reqHdrRaw.set('user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36') 76 | } 77 | // cfworker 会把路径中的 `//` 合并成 `/` 78 | const urlStr = pathname.replace(/^(https?):\/+/, '$1://') 79 | const urlObj = newUrl(urlStr) 80 | if (!urlObj) { 81 | return makeRes('invalid proxy url: ' + urlStr, 403) 82 | } 83 | 84 | /** @type {RequestInit} */ 85 | const reqInit = { 86 | method: req.method, 87 | headers: reqHdrNew, 88 | } 89 | if (req.method === 'POST') { 90 | reqInit.body = await req.arrayBuffer() 91 | } 92 | return await proxy(urlObj, reqInit) 93 | } 94 | 95 | /** 96 | * 97 | * @param {URL} urlObj 98 | * @param {RequestInit} reqInit 99 | */ 100 | async function proxy(urlObj, reqInit) { 101 | const res = await fetch(urlObj.href, reqInit) 102 | const resHdrOld = res.headers 103 | const resHdrNew = new Headers(resHdrOld) 104 | 105 | let status = res.status 106 | 107 | resHdrNew.set('access-control-expose-headers', '*') 108 | resHdrNew.set('access-control-allow-origin', '*') 109 | 110 | resHdrNew.delete('content-security-policy') 111 | resHdrNew.delete('content-security-policy-report-only') 112 | resHdrNew.delete('clear-site-data') 113 | 114 | return new Response(res.body, { 115 | status, 116 | headers: resHdrNew, 117 | }) 118 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "Metadata", 4 | "guid": "0f34b81a-4af7-4719-9958-4cb8f680e7c6", 5 | "name": "JavScraper", 6 | "description": "Emby/Jellyfin 的一个日本电影刮削器插件,可以从某些网站抓取影片信息。", 7 | "owner": "JavScraper", 8 | "overview": "Emby/Jellyfin 的一个日本电影刮削器插件,可以从某些网站抓取影片信息。", 9 | "imageURL": "https://raw.githubusercontent.com/JavScraper/Emby.Plugins.JavScraper/master/Emby.Plugins.JavScraper/thumb.png", 10 | "versions": [ 11 | { 12 | "changelog": "增加计划任务:修复缺失的中文字幕标签,需要手动在“计划任务”中执行。", 13 | "targetAbi": "10.6.0.0", 14 | "sourceUrl": "https://github.com/JavScraper/Emby.Plugins.JavScraper/releases/download/v1.2021.0622.2145/Jellyfin.JavScraper%40v1.2021.0622.2145.zip", 15 | "timestamp": "2021-06-22T13:52:37Z", 16 | "version": "1.2021.0622.2145", 17 | "checksum": "fd81ee3cfa8f275d79dc4f81d017e50a" 18 | }, 19 | { 20 | "changelog": "增加计划任务:修复缺失的中文字幕标签,需要手动在“计划任务”中执行。", 21 | "targetAbi": "10.7.0.0", 22 | "sourceUrl": "https://github.com/JavScraper/Emby.Plugins.JavScraper/releases/download/v1.2021.0622.2145/J.ellyfin.JavScraper.Beta%40v1.2021.0622.2158.zip", 23 | "timestamp": "2021-06-22T13:58:43Z", 24 | "version": "1.2021.0622.2158", 25 | "checksum": "a41e6d54817cfd4cde3daa7b444bf11e" 26 | } 27 | ] 28 | } 29 | ] --------------------------------------------------------------------------------