koishi-plugin-anime-schedule 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config.d.ts +3 -0
- package/lib/config.js +7 -1
- package/lib/constants.d.ts +7 -0
- package/lib/constants.js +8 -1
- package/lib/services/digest.js +3 -1
- package/lib/services/localizer.d.ts +13 -0
- package/lib/services/localizer.js +83 -14
- package/lib/types.d.ts +18 -0
- package/package.json +1 -1
- package/readme.md +1 -0
package/lib/config.d.ts
CHANGED
package/lib/config.js
CHANGED
|
@@ -111,9 +111,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
111
111
|
])
|
|
112
112
|
.description('标题展示方式')
|
|
113
113
|
.default('localized'),
|
|
114
|
+
scoreTtlHours: koishi_1.Schema.number()
|
|
115
|
+
.min(1).step(1)
|
|
116
|
+
.description('评分与评价人数的缓存小时数。新番开播头几周评分天天在变,'
|
|
117
|
+
+ '因此单独给一个比标题封面短得多的保质期')
|
|
118
|
+
.default(constants_1.DEFAULT_SCORE_TTL_HOURS),
|
|
114
119
|
cacheTtlDays: koishi_1.Schema.number()
|
|
115
120
|
.min(1).step(1)
|
|
116
|
-
.description('
|
|
121
|
+
.description('标题与封面的缓存天数')
|
|
117
122
|
.default(30),
|
|
118
123
|
airingTtlHours: koishi_1.Schema.number()
|
|
119
124
|
.min(1).step(1)
|
|
@@ -204,6 +209,7 @@ function normalizeConfig(config) {
|
|
|
204
209
|
localize: {
|
|
205
210
|
...config.localize,
|
|
206
211
|
cacheTtlDays: int(config.localize?.cacheTtlDays, 30, 1),
|
|
212
|
+
scoreTtlHours: int(config.localize?.scoreTtlHours, constants_1.DEFAULT_SCORE_TTL_HOURS, 1),
|
|
207
213
|
airingTtlHours: int(config.localize?.airingTtlHours, 6, 1),
|
|
208
214
|
},
|
|
209
215
|
render: {
|
package/lib/constants.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export declare const LOCALIZE_CACHE_FILE = "localize-cache.json";
|
|
|
64
64
|
* v6:新增 `hasEpisodeTimes`。逐集档期不再是每条必取,需要靠它区分
|
|
65
65
|
* 「没取过逐集档期」与「取过但没有集数日期」。
|
|
66
66
|
*/
|
|
67
|
+
/**
|
|
68
|
+
* 评分与评价人数的缓存小时数。
|
|
69
|
+
*
|
|
70
|
+
* 比标题封面的 cacheTtlDays 短得多:新番开播头几周评分天天在变,样本量从个位数
|
|
71
|
+
* 涨到几百,用 30 天缓存会一直显示「只有十几个人投票」时的噪声值。
|
|
72
|
+
*/
|
|
73
|
+
export declare const DEFAULT_SCORE_TTL_HOURS = 24;
|
|
67
74
|
export declare const LOCALIZE_CACHE_VERSION = 6;
|
|
68
75
|
/** 本地化缓存最大条目数,超出后按写入时间淘汰最旧的一半 */
|
|
69
76
|
export declare const LOCALIZE_CACHE_MAX_ENTRIES = 5000;
|
package/lib/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LOG_MESSAGES = exports.RESPONSES = exports.MESSAGES = exports.IMAGE_THEME = exports.IMAGE_LAYOUT = exports.DEFAULT_FONT_FAMILY = exports.DEFAULT_JPEG_QUALITY = exports.BOLD_FONT_CANDIDATES = exports.FONT_CANDIDATES = exports.COVER_CACHE_MAX_FILES = exports.COVER_CACHE_TTL_MS = exports.COVER_CACHE_DIR = exports.COVER_MAX_BYTES = exports.COVER_TIMEOUT_MS = exports.COVER_CONCURRENCY = exports.LOCALIZE_CONCURRENCY = exports.EPISODE_QUERY_LIMIT = exports.LOCALIZE_CACHE_MAX_ENTRIES = exports.LOCALIZE_CACHE_VERSION = exports.LOCALIZE_CACHE_FILE = exports.LATE_NIGHT_END_HOUR = exports.IMAGE_LIST_TWO_COLUMN_AT = exports.IMAGE_MAX_CARDS = exports.DIGEST_LABELS = exports.PLUGIN_HOMEPAGE = exports.PLUGIN_VERSION = exports.DATA_DIR = exports.BANGUMI_DATA_FILE = exports.BANGUMI_DATA_FALLBACK_URLS = exports.ANILIST_MIN_INTERVAL_MS = exports.ANILIST_BATCH_SIZE = exports.ANILIST_API_URL = exports.BANGUMI_API_HOST = exports.PLUGIN_NAME = void 0;
|
|
3
|
+
exports.LOG_MESSAGES = exports.RESPONSES = exports.MESSAGES = exports.IMAGE_THEME = exports.IMAGE_LAYOUT = exports.DEFAULT_FONT_FAMILY = exports.DEFAULT_JPEG_QUALITY = exports.BOLD_FONT_CANDIDATES = exports.FONT_CANDIDATES = exports.COVER_CACHE_MAX_FILES = exports.COVER_CACHE_TTL_MS = exports.COVER_CACHE_DIR = exports.COVER_MAX_BYTES = exports.COVER_TIMEOUT_MS = exports.COVER_CONCURRENCY = exports.LOCALIZE_CONCURRENCY = exports.EPISODE_QUERY_LIMIT = exports.LOCALIZE_CACHE_MAX_ENTRIES = exports.LOCALIZE_CACHE_VERSION = exports.DEFAULT_SCORE_TTL_HOURS = exports.LOCALIZE_CACHE_FILE = exports.LATE_NIGHT_END_HOUR = exports.IMAGE_LIST_TWO_COLUMN_AT = exports.IMAGE_MAX_CARDS = exports.DIGEST_LABELS = exports.PLUGIN_HOMEPAGE = exports.PLUGIN_VERSION = exports.DATA_DIR = exports.BANGUMI_DATA_FILE = exports.BANGUMI_DATA_FALLBACK_URLS = exports.ANILIST_MIN_INTERVAL_MS = exports.ANILIST_BATCH_SIZE = exports.ANILIST_API_URL = exports.BANGUMI_API_HOST = exports.PLUGIN_NAME = void 0;
|
|
4
4
|
exports.buildUserAgent = buildUserAgent;
|
|
5
5
|
exports.getMessages = getMessages;
|
|
6
6
|
/** 插件名,同时作为日志前缀与 User-Agent 主体 */
|
|
@@ -80,6 +80,13 @@ exports.LOCALIZE_CACHE_FILE = 'localize-cache.json';
|
|
|
80
80
|
* v6:新增 `hasEpisodeTimes`。逐集档期不再是每条必取,需要靠它区分
|
|
81
81
|
* 「没取过逐集档期」与「取过但没有集数日期」。
|
|
82
82
|
*/
|
|
83
|
+
/**
|
|
84
|
+
* 评分与评价人数的缓存小时数。
|
|
85
|
+
*
|
|
86
|
+
* 比标题封面的 cacheTtlDays 短得多:新番开播头几周评分天天在变,样本量从个位数
|
|
87
|
+
* 涨到几百,用 30 天缓存会一直显示「只有十几个人投票」时的噪声值。
|
|
88
|
+
*/
|
|
89
|
+
exports.DEFAULT_SCORE_TTL_HOURS = 24;
|
|
83
90
|
exports.LOCALIZE_CACHE_VERSION = 6;
|
|
84
91
|
/** 本地化缓存最大条目数,超出后按写入时间淘汰最旧的一半 */
|
|
85
92
|
exports.LOCALIZE_CACHE_MAX_ENTRIES = 5000;
|
package/lib/services/digest.js
CHANGED
|
@@ -780,7 +780,9 @@ class DigestService {
|
|
|
780
780
|
// 取不到(无 aniListId、没查到状态、或那条没给图)时回退 Bangumi。
|
|
781
781
|
coverUrl: aniList?.coverUrl || (entry.bangumiId !== null ? (subject?.coverUrl ?? '') : ''),
|
|
782
782
|
scoreValue,
|
|
783
|
-
score
|
|
783
|
+
// 两位小数:`score` 由 `rating.count` 算出的算术平均得来(见 `LocalizeCacheEntry.score`),
|
|
784
|
+
// 比 Bangumi 自己给的一位小数更有区分度;四舍五入到一位后与网站显示一致
|
|
785
|
+
score: scoreValue !== null ? scoreValue.toFixed(2) : '',
|
|
784
786
|
firstYear: firstDate ? Number(firstDate.slice(0, 4)) : null,
|
|
785
787
|
firstMonth: firstDate ? Number(firstDate.slice(5, 7)) : null,
|
|
786
788
|
subscribed: entry.bangumiId !== null && subscribed.has(`bgm:${entry.bangumiId}`),
|
|
@@ -37,6 +37,19 @@ export declare class Localizer {
|
|
|
37
37
|
private scheduleFlush;
|
|
38
38
|
/** 主动落盘,插件卸载与调度结束后调用 */
|
|
39
39
|
save(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* 这一条缓存是否还够用。
|
|
42
|
+
*
|
|
43
|
+
* 拆成「标题封面」与「评分」两个保质期,因为两者变化速度差很多:
|
|
44
|
+
*
|
|
45
|
+
* - **标题与封面**几乎不变,用 `cacheTtlDays`(默认 30 天);
|
|
46
|
+
* - **评分**在新番开播头几周天天在变(样本量从个位数涨到几百),用 `scoreTtlHours`
|
|
47
|
+
* (默认 24 小时)。早先两者共用 30 天,导致「只有 1 个人投票」时的 7.0 被当成
|
|
48
|
+
* 评分显示整整一个月,而网站早已是 6.1。
|
|
49
|
+
*
|
|
50
|
+
* @param wantEpisodeTimes 调用方这次是否需要逐集档期。需要时,缺档期的条目即使标题
|
|
51
|
+
* 还新鲜也得重取一次;不需要时(本季表、检索这类纯展示路径)就不为它白跑请求。
|
|
52
|
+
*/
|
|
40
53
|
private isFresh;
|
|
41
54
|
/**
|
|
42
55
|
* 批量取回条目详情,返回「条目 ID → 详情」。
|
|
@@ -21,6 +21,42 @@ function resolveEpisodeTotal(subject) {
|
|
|
21
21
|
return eps;
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 算评分,保留两位小数。
|
|
26
|
+
*
|
|
27
|
+
* **不用 `rating.score`**:Bangumi 只把它保留一位小数(实测 60 个条目里带两位小数的为 0),
|
|
28
|
+
* 直接拿来显示,第二位永远是 0,没有信息量。API 同时给了 `rating.count`
|
|
29
|
+
* (1~10 各分数段的人数),据此算**算术平均**即可拿到真实精度:
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* 至高之力 score=6.1 count 平均=6.0833
|
|
33
|
+
* 药屋第三季 score=7.3 count 平均=7.3231
|
|
34
|
+
* 药屋第一季 score=7.5 count 平均=7.4764
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* 与网站显示的关系:四舍五入到一位后与 `score` 一致(`6.0833 → 6.1`、`7.3231 → 7.3`),
|
|
38
|
+
* 因此不会出现「插件和网站对不上」的观感,只是多给了两位真实精度。
|
|
39
|
+
*
|
|
40
|
+
* `count` 缺失(老条目或没投票)时退回 `score`;两者都没有则返回 null。
|
|
41
|
+
*/
|
|
42
|
+
function computeScore(rating) {
|
|
43
|
+
const count = rating?.count;
|
|
44
|
+
if (count && typeof count === 'object') {
|
|
45
|
+
let sum = 0;
|
|
46
|
+
let votes = 0;
|
|
47
|
+
for (const [key, value] of Object.entries(count)) {
|
|
48
|
+
const score = Number(key);
|
|
49
|
+
const people = Number(value);
|
|
50
|
+
if (!Number.isFinite(score) || !Number.isFinite(people) || people <= 0)
|
|
51
|
+
continue;
|
|
52
|
+
sum += score * people;
|
|
53
|
+
votes += people;
|
|
54
|
+
}
|
|
55
|
+
if (votes > 0)
|
|
56
|
+
return Math.round((sum / votes) * 100) / 100;
|
|
57
|
+
}
|
|
58
|
+
return typeof rating?.score === 'number' ? rating.score : null;
|
|
59
|
+
}
|
|
24
60
|
/**
|
|
25
61
|
* 兼容不同的 http 适配器:Koishi 的 http 服务返回响应体本身,
|
|
26
62
|
* 而直接使用 axios 的调用方会拿到完整的响应对象。
|
|
@@ -135,14 +171,31 @@ class Localizer {
|
|
|
135
171
|
}
|
|
136
172
|
await this.flush();
|
|
137
173
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
/**
|
|
175
|
+
* 这一条缓存是否还够用。
|
|
176
|
+
*
|
|
177
|
+
* 拆成「标题封面」与「评分」两个保质期,因为两者变化速度差很多:
|
|
178
|
+
*
|
|
179
|
+
* - **标题与封面**几乎不变,用 `cacheTtlDays`(默认 30 天);
|
|
180
|
+
* - **评分**在新番开播头几周天天在变(样本量从个位数涨到几百),用 `scoreTtlHours`
|
|
181
|
+
* (默认 24 小时)。早先两者共用 30 天,导致「只有 1 个人投票」时的 7.0 被当成
|
|
182
|
+
* 评分显示整整一个月,而网站早已是 6.1。
|
|
183
|
+
*
|
|
184
|
+
* @param wantEpisodeTimes 调用方这次是否需要逐集档期。需要时,缺档期的条目即使标题
|
|
185
|
+
* 还新鲜也得重取一次;不需要时(本季表、检索这类纯展示路径)就不为它白跑请求。
|
|
186
|
+
*/
|
|
187
|
+
isFresh(entry, wantEpisodeTimes) {
|
|
188
|
+
if (entry.missing) {
|
|
189
|
+
// 「不存在」也是有效结果:同一季内不必反复去问一个不会出现的条目
|
|
141
190
|
return Date.now() - entry.cachedAt < this.config.localize.cacheTtlDays * 24 * 60 * 60 * 1000;
|
|
142
|
-
|
|
143
|
-
//
|
|
191
|
+
}
|
|
192
|
+
// 取过条目详情、却没取逐集档期,而这次需要档期 → 必须重取。
|
|
144
193
|
// 旧版缓存没有 hasEpisodeTimes 字段,按「取过」处理。
|
|
145
|
-
if (entry.hasEpisodeTimes === false)
|
|
194
|
+
if (wantEpisodeTimes && entry.hasEpisodeTimes === false)
|
|
195
|
+
return false;
|
|
196
|
+
// 评分单独算保质期:到期就重取详情,但重取时仍按调用方的需要决定要不要连档期一起取
|
|
197
|
+
const scoreAt = entry.scoreCachedAt ?? entry.cachedAt;
|
|
198
|
+
if (Date.now() - scoreAt >= this.config.localize.scoreTtlHours * 60 * 60 * 1000)
|
|
146
199
|
return false;
|
|
147
200
|
return Date.now() - entry.cachedAt < this.config.localize.cacheTtlDays * 24 * 60 * 60 * 1000;
|
|
148
201
|
}
|
|
@@ -162,6 +215,8 @@ class Localizer {
|
|
|
162
215
|
return result;
|
|
163
216
|
await this.load();
|
|
164
217
|
const wanted = [];
|
|
218
|
+
/** 这次要重取的条目里,缓存中已有的那一份;用于判断是否还需要补逐集档期 */
|
|
219
|
+
const previous = new Map();
|
|
165
220
|
const seen = new Set();
|
|
166
221
|
for (const id of ids) {
|
|
167
222
|
if (!Number.isFinite(id) || seen.has(id))
|
|
@@ -174,16 +229,24 @@ class Localizer {
|
|
|
174
229
|
}
|
|
175
230
|
// 过期条目也先返回,避免一次请求失败就让整张表掉封面
|
|
176
231
|
result.set(id, cached);
|
|
177
|
-
if (!this.isFresh(cached))
|
|
232
|
+
if (!this.isFresh(cached, wantEpisodeTimes)) {
|
|
178
233
|
wanted.push(id);
|
|
234
|
+
previous.set(id, cached);
|
|
235
|
+
}
|
|
179
236
|
}
|
|
180
237
|
if (!wanted.length)
|
|
181
238
|
return result;
|
|
182
239
|
const started = Date.now();
|
|
183
240
|
const looked = await (0, utils_1.mapLimit)(wanted, constants_1.LOCALIZE_CONCURRENCY, async (id) => {
|
|
241
|
+
const stale = previous.get(id);
|
|
242
|
+
// 逐集档期只在「调用方要」且「有正规集数」时才值得请求:用它拿首末集日期。
|
|
243
|
+
//
|
|
244
|
+
// 已经取过档期的条目**不重复取**——评分每天刷新,而档期是另一笔请求,
|
|
245
|
+
// 跟着一起重发会让请求量凭空翻倍(周表 77 部就是多 77 个请求)。
|
|
246
|
+
// 只有「没取过」或「这次确实缺它」才补。
|
|
247
|
+
const needsTimes = wantEpisodeTimes && !(stale && stale.hasEpisodeTimes !== false);
|
|
184
248
|
const subject = await this.fetchSubject(id);
|
|
185
|
-
|
|
186
|
-
const times = wantEpisodeTimes && subject && resolveEpisodeTotal(subject)
|
|
249
|
+
const times = needsTimes && subject && resolveEpisodeTotal(subject)
|
|
187
250
|
? await this.fetchEpisodeTimes(id)
|
|
188
251
|
: null;
|
|
189
252
|
return { id, subject, times };
|
|
@@ -193,18 +256,24 @@ class Localizer {
|
|
|
193
256
|
if (!subject)
|
|
194
257
|
continue;
|
|
195
258
|
ok++;
|
|
259
|
+
const stale = previous.get(id);
|
|
260
|
+
// 这次没重新取档期(评分刷新)时,沿用上一次的结果,别把它当「没有档期」
|
|
261
|
+
const hasTimes = times !== null || stale?.hasEpisodeTimes === true;
|
|
196
262
|
const entry = {
|
|
197
263
|
subjectId: subject.id,
|
|
198
264
|
title: subject.name_cn?.trim() || subject.name,
|
|
199
265
|
coverUrl: subject.images?.large ?? subject.images?.common ?? '',
|
|
200
|
-
score:
|
|
266
|
+
score: computeScore(subject.rating),
|
|
201
267
|
ratingTotal: typeof subject.rating?.total === 'number' ? subject.rating.total : null,
|
|
202
268
|
totalEpisodes: resolveEpisodeTotal(subject),
|
|
203
|
-
hasEpisodeTimes:
|
|
204
|
-
firstAirMs: times?.first ?? null,
|
|
205
|
-
lastAirMs: times?.last ?? null,
|
|
269
|
+
hasEpisodeTimes: hasTimes,
|
|
270
|
+
firstAirMs: times?.first ?? stale?.firstAirMs ?? null,
|
|
271
|
+
lastAirMs: times?.last ?? stale?.lastAirMs ?? null,
|
|
206
272
|
airDate: subject.date ?? null,
|
|
207
|
-
cachedAt
|
|
273
|
+
// `cachedAt` 只在真的取全(含档期)时推进,否则标题封面的 30 天保质期会被
|
|
274
|
+
// 每天一次的评分刷新无限续期,等于永远不校验
|
|
275
|
+
cachedAt: times !== null || !stale ? Date.now() : stale.cachedAt,
|
|
276
|
+
scoreCachedAt: Date.now(),
|
|
208
277
|
};
|
|
209
278
|
this.memory.set(id, entry);
|
|
210
279
|
this.scheduleFlush();
|
package/lib/types.d.ts
CHANGED
|
@@ -70,8 +70,26 @@ export interface LocalizeCacheEntry {
|
|
|
70
70
|
/** 中文名,缺失时回落为日文原名 */
|
|
71
71
|
title: string;
|
|
72
72
|
coverUrl: string;
|
|
73
|
+
/**
|
|
74
|
+
* 评分,两位小数。
|
|
75
|
+
*
|
|
76
|
+
* 由 `rating.count`(各分数段人数)算出的**算术平均**,而不是 Bangumi 直接给的
|
|
77
|
+
* `rating.score`——后者只保留一位小数(实测 60 个条目里带两位小数的为 0),
|
|
78
|
+
* 自己算才有第二位。四舍五入到一位后与网站显示一致,不会造成「对不上」的观感。
|
|
79
|
+
* `rating.count` 缺失时退回 `rating.score`。
|
|
80
|
+
*/
|
|
73
81
|
score: number | null;
|
|
74
82
|
ratingTotal: number | null;
|
|
83
|
+
/**
|
|
84
|
+
* 评分与 `ratingTotal` 的取回时刻。
|
|
85
|
+
*
|
|
86
|
+
* 与 `cachedAt` 分开,因为两者保质期差很多:标题与封面几乎不变(`cacheTtlDays`,
|
|
87
|
+
* 默认 30 天),而**新番的评分在开播头几周会剧烈变化**(样本量从个位数涨到几百),
|
|
88
|
+
* 用 30 天缓存会一直显示「只有十几个人投票」时的噪声值。见 `localize.scoreTtlHours`。
|
|
89
|
+
*
|
|
90
|
+
* 旧缓存没有这个字段,按 `cachedAt` 处理(即可能立刻被刷新一次)。
|
|
91
|
+
*/
|
|
92
|
+
scoreCachedAt?: number;
|
|
75
93
|
/**
|
|
76
94
|
* Bangumi 给出的总集数,仅作缓存留档。
|
|
77
95
|
*
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -56,6 +56,7 @@ npm i koishi-plugin-anime-schedule
|
|
|
56
56
|
| `output.format` | `image` | `image` / `text` |
|
|
57
57
|
| `output.layout` | `table` | 图片排版:`table` / `list` |
|
|
58
58
|
| `localize.titleStyle` | `localized` | `localized` 仅中文 / `both` 中文(日文原名)/ `original` 仅日文原名 |
|
|
59
|
+
| `localize.scoreTtlHours` | `24` | 评分与评价人数的缓存小时数。新番开播头几周评分天天在变,因此单独给一个较短的保质期 |
|
|
59
60
|
| `render.jpegQuality` | `100` | 图片编码质量,调低可减小体积 |
|
|
60
61
|
| `advanced.debug` | `false` | 输出调试日志 |
|
|
61
62
|
|