[] {
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/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/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/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.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 | }
--------------------------------------------------------------------------------