[] {
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