koishi-plugin-anime-schedule 1.0.8 → 1.0.9

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.
Files changed (47) hide show
  1. package/lib/config.d.ts +6 -5
  2. package/lib/config.js +35 -21
  3. package/lib/constants.d.ts +69 -15
  4. package/lib/constants.js +82 -23
  5. package/lib/data/anilist-match.d.ts +7 -16
  6. package/lib/data/anilist-match.js +7 -16
  7. package/lib/data/bangumi-data-loader.js +2 -5
  8. package/lib/data/bangumi-data-source.d.ts +6 -20
  9. package/lib/data/bangumi-data-source.js +6 -19
  10. package/lib/data/bangumi-data.d.ts +24 -46
  11. package/lib/data/bangumi-data.js +18 -27
  12. package/lib/handlers.d.ts +17 -21
  13. package/lib/handlers.js +128 -71
  14. package/lib/index.d.ts +1 -1
  15. package/lib/index.js +23 -19
  16. package/lib/render/canvas.d.ts +11 -11
  17. package/lib/render/canvas.js +24 -12
  18. package/lib/render/covers.d.ts +2 -8
  19. package/lib/render/covers.js +6 -20
  20. package/lib/render/fonts.d.ts +5 -11
  21. package/lib/render/fonts.js +44 -12
  22. package/lib/render/image.d.ts +53 -76
  23. package/lib/render/image.js +224 -202
  24. package/lib/render/info.d.ts +2 -28
  25. package/lib/render/info.js +182 -167
  26. package/lib/render/text.d.ts +9 -56
  27. package/lib/render/text.js +21 -68
  28. package/lib/services/airing.d.ts +17 -43
  29. package/lib/services/airing.js +17 -34
  30. package/lib/services/digest.d.ts +76 -233
  31. package/lib/services/digest.js +151 -365
  32. package/lib/services/dispatch.d.ts +7 -15
  33. package/lib/services/dispatch.js +7 -15
  34. package/lib/services/info.d.ts +51 -34
  35. package/lib/services/info.js +143 -50
  36. package/lib/services/localizer.d.ts +20 -45
  37. package/lib/services/localizer.js +49 -101
  38. package/lib/services/network.d.ts +23 -22
  39. package/lib/services/network.js +43 -12
  40. package/lib/services/scheduler.d.ts +11 -22
  41. package/lib/services/scheduler.js +11 -22
  42. package/lib/types.d.ts +41 -74
  43. package/lib/types.js +1 -6
  44. package/lib/utils/time.d.ts +1 -1
  45. package/lib/utils/time.js +1 -1
  46. package/package.json +1 -1
  47. package/readme.md +19 -21
package/lib/config.d.ts CHANGED
@@ -28,6 +28,8 @@ export interface Config {
28
28
  format: DigestFormat;
29
29
  layout: ImageLayout;
30
30
  coverLimit: number;
31
+ /** `anime.info` 按关键词查询时最多出几部,`0` 表示不限制 */
32
+ infoKeywordLimit: number;
31
33
  showCover: boolean;
32
34
  showScore: boolean;
33
35
  useForward: boolean;
@@ -46,8 +48,10 @@ export interface Config {
46
48
  fontFamily: string;
47
49
  coverConcurrency: number;
48
50
  coverCacheDays: number;
49
- /** JPEG 编码质量(0-100)。图片以文字与线条为主,压缩伪影很明显,因此默认取高值 */
51
+ /** JPEG 编码质量(1-100) */
50
52
  jpegQuality: number;
53
+ /** 超采样倍数:画布按「逻辑尺寸 × 该值」建立后再等比缩放,小字笔画更锐利,体积与耗时随之上升 */
54
+ supersample: number;
51
55
  };
52
56
  subscription: {
53
57
  /** 单个群最多订阅的番剧数量,0 表示不限制 */
@@ -60,8 +64,5 @@ export interface Config {
60
64
  };
61
65
  }
62
66
  export declare const Config: Schema<Config>;
63
- /**
64
- * 把配置夹到安全区间。
65
- * 只保证「类型正确」与「不小于最小值」,不设人为上限。
66
- */
67
+ /** 把配置夹到安全区间:只保证类型正确且不小于最小值,不设人为上限 */
67
68
  export declare function normalizeConfig(config: Config): Config;
package/lib/config.js CHANGED
@@ -24,7 +24,7 @@ exports.Config = koishi_1.Schema.intersect([
24
24
  koishi_1.Schema.const('Asia/Taipei').description('Asia/Taipei (UTC+8)'),
25
25
  koishi_1.Schema.const('UTC').description('UTC'),
26
26
  ])
27
- .description('时刻与日期按此时区换算')
27
+ .description('时刻与日期换算时区')
28
28
  .default('Asia/Shanghai'),
29
29
  locale: koishi_1.Schema.union([
30
30
  koishi_1.Schema.const('zh-CN').description('简体中文'),
@@ -39,10 +39,10 @@ exports.Config = koishi_1.Schema.intersect([
39
39
  dailyEnabled: koishi_1.Schema.boolean().description('每日更新表').default(true),
40
40
  dailyTime: koishi_1.Schema.string().pattern(TIME_PATTERN).description('推送时刻').default('08:00'),
41
41
  dailyOffsetDays: koishi_1.Schema.number()
42
- .min(-1).max(1).step(1)
43
- .description('日期偏移:0 当天,-1 前一天,1 后一天')
42
+ .min(constants_1.DAILY_OFFSET_MIN).max(constants_1.DAILY_OFFSET_MAX).step(1)
43
+ .description('日期偏移,0 当天、-1 前一天、1 后一天')
44
44
  .default(0),
45
- nightEnabled: koishi_1.Schema.boolean().description('深夜档(当晚 21:00 至次日 06:00)').default(true),
45
+ nightEnabled: koishi_1.Schema.boolean().description('深夜档,当晚 21:00 至次日 06:00').default(true),
46
46
  nightTime: koishi_1.Schema.string().pattern(TIME_PATTERN).description('推送时刻').default('21:00'),
47
47
  weeklyEnabled: koishi_1.Schema.boolean().description('本周更新表').default(true),
48
48
  weeklyWeekday: koishi_1.Schema.union(WEEKDAYS.map((item) => koishi_1.Schema.const(item.value).description(item.label)))
@@ -66,11 +66,11 @@ exports.Config = koishi_1.Schema.intersect([
66
66
  content: koishi_1.Schema.object({
67
67
  maxItems: koishi_1.Schema.number()
68
68
  .min(0).step(10)
69
- .description('单次推送最多显示多少部,0 为不限制')
69
+ .description('单次推送最多显示多少部,0 不限制')
70
70
  .default(0),
71
71
  longRunningThreshold: koishi_1.Schema.number()
72
72
  .min(0).step(1)
73
- .description('剔除长期连载番的话数阈值,0 为不过滤')
73
+ .description('剔除长期连载番的话数阈值,0 不过滤')
74
74
  .default(0),
75
75
  }),
76
76
  }).description('推送内容'),
@@ -80,7 +80,7 @@ exports.Config = koishi_1.Schema.intersect([
80
80
  koishi_1.Schema.const('image').description('风格化图片'),
81
81
  koishi_1.Schema.const('text').description('纯文本'),
82
82
  ])
83
- .description('推送形态;图片渲染失败时自动改用文本')
83
+ .description('推送形态,图片渲染失败时自动改用文本')
84
84
  .default('image'),
85
85
  layout: koishi_1.Schema.union([
86
86
  koishi_1.Schema.const('table').description('时间表'),
@@ -90,12 +90,16 @@ exports.Config = koishi_1.Schema.intersect([
90
90
  .default('table'),
91
91
  coverLimit: koishi_1.Schema.number()
92
92
  .min(0)
93
- .description('单张图片最多加载多少张封面,0 为不限制')
93
+ .description('单张图片最多加载多少张封面,0 不限制')
94
94
  .default(0),
95
+ infoKeywordLimit: koishi_1.Schema.number()
96
+ .min(0)
97
+ .description('anime.info 按关键词查询番剧时显示数量上限,0 不限制')
98
+ .default(20),
95
99
  showCover: koishi_1.Schema.boolean().description('显示封面').default(true),
96
- showScore: koishi_1.Schema.boolean().description('显示评分,评价不足 10 人时不显示').default(true),
100
+ showScore: koishi_1.Schema.boolean().description('显示评分,评价不足 10 条时隐藏').default(true),
97
101
  useForward: koishi_1.Schema.boolean()
98
- .description('纯文本模式使用合并转发,避免刷屏')
102
+ .description('纯文本模式用合并转发,避免刷屏')
99
103
  .default(true),
100
104
  }),
101
105
  }).description('输出设置'),
@@ -121,14 +125,14 @@ exports.Config = koishi_1.Schema.intersect([
121
125
  .default(30),
122
126
  airingTtlHours: koishi_1.Schema.number()
123
127
  .min(1).step(1)
124
- .description('播出状态(是否已播完)的缓存小时数')
128
+ .description('是否已播完的缓存小时数')
125
129
  .default(6),
126
130
  }),
127
131
  }).description('本地化'),
128
132
  koishi_1.Schema.object({
129
133
  render: koishi_1.Schema.object({
130
134
  fontFamily: koishi_1.Schema.string()
131
- .description('绘制图片时使用的字体族;留空则自动寻找系统中文字体')
135
+ .description('字体族,留空则自动寻找系统中文字体')
132
136
  .default(constants_1.DEFAULT_FONT_FAMILY),
133
137
  coverConcurrency: koishi_1.Schema.number()
134
138
  .min(1)
@@ -136,19 +140,23 @@ exports.Config = koishi_1.Schema.intersect([
136
140
  .default(constants_1.COVER_CONCURRENCY),
137
141
  coverCacheDays: koishi_1.Schema.number()
138
142
  .min(0)
139
- .description('封面缓存天数,0 为不写磁盘')
143
+ .description('封面缓存天数,0 不写磁盘')
140
144
  .default(21),
141
145
  jpegQuality: koishi_1.Schema.number()
142
146
  .min(1).max(100).step(1)
143
- .description('图片编码质量')
147
+ .description('编码质量')
144
148
  .default(constants_1.DEFAULT_JPEG_QUALITY),
149
+ supersample: koishi_1.Schema.number()
150
+ .min(1).max(constants_1.MAX_SUPERSAMPLE).step(1)
151
+ .description('超采样倍数')
152
+ .default(constants_1.DEFAULT_SUPERSAMPLE),
145
153
  }),
146
154
  }).description('图片'),
147
155
  koishi_1.Schema.object({
148
156
  subscription: koishi_1.Schema.object({
149
157
  maxPerChannel: koishi_1.Schema.number()
150
158
  .min(0).step(1)
151
- .description('单个群最多订阅多少部,0 为不限制')
159
+ .description('单个群最多订阅多少部,0 不限制')
152
160
  .default(0),
153
161
  }),
154
162
  }).description('订阅'),
@@ -168,18 +176,22 @@ exports.Config = koishi_1.Schema.intersect([
168
176
  }),
169
177
  }).description('其他'),
170
178
  ]);
171
- /**
172
- * 把配置夹到安全区间。
173
- * 只保证「类型正确」与「不小于最小值」,不设人为上限。
174
- */
179
+ /** 把配置夹到安全区间:只保证类型正确且不小于最小值,不设人为上限 */
175
180
  function normalizeConfig(config) {
181
+ /** 取整并夹到不小于 `min`(默认 0)。允许负值的字段不能走这个默认值 */
176
182
  const int = (value, fallback, min = 0) => {
177
183
  if (typeof value !== 'number' || !Number.isFinite(value))
178
184
  return fallback;
179
185
  return Math.max(min, Math.round(value));
180
186
  };
187
+ /** 取整并夹到 `[min, max]`;用于允许负值的字段,如 `dailyOffsetDays` */
188
+ const intBetween = (value, fallback, min, max) => {
189
+ if (typeof value !== 'number' || !Number.isFinite(value))
190
+ return fallback;
191
+ return Math.min(max, Math.max(min, Math.round(value)));
192
+ };
181
193
  const time = (value, fallback) => (typeof value === 'string' && TIME_PATTERN.test(value) ? value : fallback);
182
- // 旧版本存在 `both` 形态,已移除,统一按图片处理
194
+ // 旧版本的 `both` 形态已移除:非 `text` 一律按图片处理
183
195
  const format = config.output?.format === 'text' ? 'text' : 'image';
184
196
  return {
185
197
  ...config,
@@ -187,7 +199,7 @@ function normalizeConfig(config) {
187
199
  push: {
188
200
  ...config.push,
189
201
  dailyTime: time(config.push?.dailyTime, '08:00'),
190
- dailyOffsetDays: Math.max(-1, Math.min(1, int(config.push?.dailyOffsetDays, 0))),
202
+ dailyOffsetDays: intBetween(config.push?.dailyOffsetDays, 0, constants_1.DAILY_OFFSET_MIN, constants_1.DAILY_OFFSET_MAX),
191
203
  weeklyWeekday: Math.max(0, Math.min(6, int(config.push?.weeklyWeekday, 1))),
192
204
  nightTime: time(config.push?.nightTime, '21:00'),
193
205
  weeklyTime: time(config.push?.weeklyTime, '08:00'),
@@ -204,6 +216,7 @@ function normalizeConfig(config) {
204
216
  format,
205
217
  layout: config.output?.layout === 'list' ? 'list' : 'table',
206
218
  coverLimit: int(config.output?.coverLimit, 0),
219
+ infoKeywordLimit: int(config.output?.infoKeywordLimit, 20),
207
220
  },
208
221
  localize: {
209
222
  ...config.localize,
@@ -217,6 +230,7 @@ function normalizeConfig(config) {
217
230
  coverConcurrency: int(config.render?.coverConcurrency, constants_1.COVER_CONCURRENCY, 1),
218
231
  coverCacheDays: int(config.render?.coverCacheDays, 21),
219
232
  jpegQuality: Math.min(100, int(config.render?.jpegQuality, constants_1.DEFAULT_JPEG_QUALITY, 1)),
233
+ supersample: Math.min(constants_1.MAX_SUPERSAMPLE, int(config.render?.supersample, constants_1.DEFAULT_SUPERSAMPLE, 1)),
220
234
  },
221
235
  subscription: {
222
236
  ...config.subscription,
@@ -74,9 +74,8 @@ export declare const DEFAULT_SCORE_TTL_HOURS = 24;
74
74
  /**
75
75
  * 显示评分所需的最少评价人数。
76
76
  *
77
- * 低于这个数就不显示评分:新番刚开播时可能只有一两个人投票,那个数字会随着样本增加
78
- * 剧烈变化(实测至高之力 1 票时是 7.0,156 票时是 6.1)。宁可留空,也不要给读者一个
79
- * 会误导人的数字。
77
+ * 低于此数不显示评分:新番开播初期样本极少,评分会随样本增加剧烈变化
78
+ * (实测至高之力 1 票时 7.0,156 票时 6.1)。留空优于给出误导性的数字。
80
79
  */
81
80
  export declare const MIN_SCORE_VOTES = 10;
82
81
  export declare const LOCALIZE_CACHE_VERSION = 6;
@@ -102,15 +101,43 @@ export declare const COVER_CACHE_MAX_FILES = 1200;
102
101
  /** 图片渲染用的字体候选,按平台常见路径排列,找不到时回退到运行环境默认字体 */
103
102
  export declare const FONT_CANDIDATES: string[];
104
103
  /** 加粗字体候选,找不到时由绘制层用描边模拟粗体 */
104
+ /**
105
+ * 兜底字体族候选。
106
+ *
107
+ * 与 `FONT_CANDIDATES` 的区别:那些是**主字体**(选一个用),这些是**补充**
108
+ * (全部注册,缺字形时按顺序回退)。`@napi-rs/canvas` 只在"字体族不存在"时回退,
109
+ * 族存在但缺字形就直接画豆腐块,所以补充族必须真的注册进来才有效。
110
+ * 名字要与 `render/fonts.ts` 的 `FALLBACK_FAMILIES` 一致。
111
+ */
112
+ export declare const FALLBACK_FONT_CANDIDATES: string[];
105
113
  export declare const BOLD_FONT_CANDIDATES: string[];
106
114
  /**
107
115
  * JPEG 编码质量。
108
116
  *
109
- * 时间表以文字与细线条为主,JPEG 的块状伪影在这种内容上比照片里明显得多。取 100
110
- * 是**无损**编码,文字边缘最锐利;体积比 92 大约多三成,但一张周表也就一兆出头,
111
- * 不值得为省这点流量牺牲清晰度。想压体积可以调低。
117
+ * 图片以文字与细线条为主,块状伪影比在照片上明显。取 100 为无损编码,文字边缘最锐利;
118
+ * 体积比 92 约多三成,可调低换取更小体积。
112
119
  */
113
120
  export declare const DEFAULT_JPEG_QUALITY = 100;
121
+ /**
122
+ * 超采样倍数。
123
+ *
124
+ * 画布按「逻辑尺寸 × 该值」建立后在上下文上做等比 `scale`,缩回目标尺寸时由更多采样
125
+ * 得出边缘像素。清晰度主要取决于此:各版面最小字号仅 10~11px,1× 下一个字不足十几个
126
+ * 像素,JPEG 质量再高也无法还原。
127
+ *
128
+ * 3 在密集的封面与小字上收益仍可见;5 是收益递减的上限。
129
+ */
130
+ export declare const DEFAULT_SUPERSAMPLE = 3;
131
+ /** 超采样倍数的可选上限,与配置项的 `.max()` 保持一致 */
132
+ export declare const MAX_SUPERSAMPLE = 5;
133
+ /**
134
+ * 每日更新表的日期偏移范围(天)。
135
+ *
136
+ * 配置项的 `.min()/.max()` 与 `normalizeConfig` 的钳制共用这两个值——
137
+ * 两处分开写时极易只改一处,导致「配置页能填、实际被悄悄夹回去」。
138
+ */
139
+ export declare const DAILY_OFFSET_MIN = -7;
140
+ export declare const DAILY_OFFSET_MAX = 7;
114
141
  /** 注册到 canvas 的字体族名,与 config.render.fontFamily 的默认值保持一致 */
115
142
  export declare const DEFAULT_FONT_FAMILY = "Anime Schedule Sans";
116
143
  /**
@@ -131,14 +158,21 @@ export declare const IMAGE_LAYOUT: {
131
158
  footerHeight: number;
132
159
  /** 周历表:七天并排的画布宽度 */
133
160
  weekWidth: number;
134
- /** 单日时间轴(每日 / 深夜档)的画布宽度 */
161
+ /**
162
+ * 单日时间轴(每日 / 深夜档)画布宽度。
163
+ *
164
+ * 等于「左边距 + 时刻栏 + 轨道 + 时刻↔卡片间距 + 卡片宽 + 右边距」,不留余量,
165
+ * 使「现在」虚线的终点与卡片右缘重合。
166
+ */
135
167
  timelineWidth: number;
136
168
  /** 卡片列表(list 排版)的画布宽度 */
137
169
  listWidth: number;
138
170
  /** 本季封面网格的画布宽度,要放得下 10 列 */
139
171
  seasonWidth: number;
140
- /** 行卡片高度(每日 / 列表排版)。容纳「标题 + 时刻 + 集数 + ID + 评分」五行 */
172
+ /** 行卡片高度:容纳固定的两行标题 + 一行元信息 */
141
173
  rowHeight: number;
174
+ /** 行卡片宽度,窄于画布,避免文字仅占左半、右侧留空 */
175
+ rowCardWidth: number;
142
176
  /** 行卡片之间的间距 */
143
177
  rowGap: number;
144
178
  /** 行卡片圆角与内边距 */
@@ -148,12 +182,15 @@ export declare const IMAGE_LAYOUT: {
148
182
  mediaHeight: number;
149
183
  /** 封面与文字之间的间距 */
150
184
  mediaGap: number;
151
- /** 卡片文字行间距。五行内容靠它撑开节奏,太小会挤成一团 */
185
+ /** 标题行高与固定行数:短标题也占两行,使元信息行在所有卡片上对齐 */
186
+ cardTitleLineHeight: number;
187
+ cardTitleLines: number;
188
+ /** 标题块与元信息行之间的间距 */
152
189
  cardLineGap: number;
153
190
  /** 封面圆角 */
154
191
  mediaRadius: number;
155
- /** 时间轴轨道相对左边距的位置 */
156
- timelineSpine: number;
192
+ /** 左侧时刻栏宽度(内容左缘到轨道)。需容纳 `现在 18:30` 这类最长时刻文本,否则被画布左缘裁掉 */
193
+ timelineLabelWidth: number;
157
194
  /** 轨道圆点半径 */
158
195
  timelineDot: number;
159
196
  /** 时刻文字与轨道之间的间距 */
@@ -176,8 +213,7 @@ export declare const IMAGE_LAYOUT: {
176
213
  weekTitleLineHeight: number;
177
214
  /** 周历表条目内的封面高度(与三行文字齐高,保持 1 : 1.4 比例不再被压扁) */
178
215
  weekMediaHeight: number;
179
- /** 列表卡片高度(与行卡片一致,两种排版观感统一) */
180
- cardHeight: number;
216
+ /** 列表卡片沿用行卡片高度,两种排版保持一致 */
181
217
  cardGap: number;
182
218
  /** 两栏之间的间距 */
183
219
  cardColumnGap: number;
@@ -205,17 +241,35 @@ export declare const IMAGE_THEME: {
205
241
  surface: string;
206
242
  cardBorder: string;
207
243
  divider: string;
208
- /** 强调色:时刻、日期与「今天」高亮 */
244
+ /**
245
+ * 强调色:时刻、日期、「今天」高亮,以及详情卡的已播集数与峰值柱。
246
+ *
247
+ * 取珊瑚红 `#e26d6d`——比纯红柔和,整张表看下来不刺眼;详情卡的实心块共用它,
248
+ * 于是「已播集数」与列表里的「已订阅」气泡是同一套视觉语言。
249
+ */
209
250
  accent: string;
210
251
  /** 封面的底色(等比完整显示时留白的那一圈) */
211
252
  badge: string;
253
+ /** 强调色的浅色底:软胶囊用 */
254
+ accentSoft: string;
255
+ /** 未播集数 / 禁用态的浅灰底 */
256
+ inert: string;
257
+ /** 未播集数的文字色,比 muted 更浅,明确表示「还没到」 */
258
+ inertText: string;
259
+ /** 未播集数的描边:仅靠底色时与白色卡片难以区分,需勾出轮廓 */
260
+ inertBorder: string;
261
+ /** 评分柱的非峰值色 */
262
+ scoreBar: string;
212
263
  title: string;
213
264
  body: string;
214
265
  muted: string;
215
266
  onAccent: string;
267
+ /** 评分:橙金色,全插件统一 */
216
268
  score: string;
217
269
  /** 卡片投影:略深一点,白色卡片才从浅灰背景上立起来 */
218
270
  shadow: string;
271
+ /** 封面投影,比卡片投影更轻 */
272
+ coverShadow: string;
219
273
  };
220
274
  /**
221
275
  * 面向用户的文本。默认中文,`ja-JP` / `en-US` 覆盖展示层最常读到的句子,
@@ -301,7 +355,7 @@ export declare const RESPONSES: {
301
355
  readonly databaseFailed: "数据库操作失败,请检查后台日志。";
302
356
  readonly renderFailed: "图片渲染失败,已改用文本发送。";
303
357
  readonly emptySearch: (keyword: string) => string;
304
- readonly notFound: "未找到该作品,它可能不在 bangumi-data 索引里。";
358
+ readonly notFound: "未找到该作品,可能不在 bangumi-data 索引中。";
305
359
  };
306
360
  /** 日志里出现的固定短语 */
307
361
  export declare const LOG_MESSAGES: {
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.MIN_SCORE_VOTES = 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;
3
+ exports.LOG_MESSAGES = exports.RESPONSES = exports.MESSAGES = exports.IMAGE_THEME = exports.IMAGE_LAYOUT = exports.DEFAULT_FONT_FAMILY = exports.DAILY_OFFSET_MAX = exports.DAILY_OFFSET_MIN = exports.MAX_SUPERSAMPLE = exports.DEFAULT_SUPERSAMPLE = exports.DEFAULT_JPEG_QUALITY = exports.BOLD_FONT_CANDIDATES = exports.FALLBACK_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.MIN_SCORE_VOTES = 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 主体 */
@@ -90,9 +90,8 @@ exports.DEFAULT_SCORE_TTL_HOURS = 24;
90
90
  /**
91
91
  * 显示评分所需的最少评价人数。
92
92
  *
93
- * 低于这个数就不显示评分:新番刚开播时可能只有一两个人投票,那个数字会随着样本增加
94
- * 剧烈变化(实测至高之力 1 票时是 7.0,156 票时是 6.1)。宁可留空,也不要给读者一个
95
- * 会误导人的数字。
93
+ * 低于此数不显示评分:新番开播初期样本极少,评分会随样本增加剧烈变化
94
+ * (实测至高之力 1 票时 7.0,156 票时 6.1)。留空优于给出误导性的数字。
96
95
  */
97
96
  exports.MIN_SCORE_VOTES = 10;
98
97
  exports.LOCALIZE_CACHE_VERSION = 6;
@@ -132,6 +131,20 @@ exports.FONT_CANDIDATES = [
132
131
  '/usr/share/fonts/truetype/arphic/uming.ttc',
133
132
  ];
134
133
  /** 加粗字体候选,找不到时由绘制层用描边模拟粗体 */
134
+ /**
135
+ * 兜底字体族候选。
136
+ *
137
+ * 与 `FONT_CANDIDATES` 的区别:那些是**主字体**(选一个用),这些是**补充**
138
+ * (全部注册,缺字形时按顺序回退)。`@napi-rs/canvas` 只在"字体族不存在"时回退,
139
+ * 族存在但缺字形就直接画豆腐块,所以补充族必须真的注册进来才有效。
140
+ * 名字要与 `render/fonts.ts` 的 `FALLBACK_FAMILIES` 一致。
141
+ */
142
+ exports.FALLBACK_FONT_CANDIDATES = [
143
+ 'C:/Windows/Fonts/YuGothR.ttc',
144
+ 'C:/Windows/Fonts/msgothic.ttc',
145
+ 'C:/Windows/Fonts/simsun.ttc',
146
+ 'C:/Windows/Fonts/NotoSansSC-VF.ttf',
147
+ ];
135
148
  exports.BOLD_FONT_CANDIDATES = [
136
149
  'C:/Windows/Fonts/msyhbd.ttc',
137
150
  'C:/Windows/Fonts/Dengb.ttf',
@@ -143,11 +156,30 @@ exports.BOLD_FONT_CANDIDATES = [
143
156
  /**
144
157
  * JPEG 编码质量。
145
158
  *
146
- * 时间表以文字与细线条为主,JPEG 的块状伪影在这种内容上比照片里明显得多。取 100
147
- * 是**无损**编码,文字边缘最锐利;体积比 92 大约多三成,但一张周表也就一兆出头,
148
- * 不值得为省这点流量牺牲清晰度。想压体积可以调低。
159
+ * 图片以文字与细线条为主,块状伪影比在照片上明显。取 100 为无损编码,文字边缘最锐利;
160
+ * 体积比 92 约多三成,可调低换取更小体积。
149
161
  */
150
162
  exports.DEFAULT_JPEG_QUALITY = 100;
163
+ /**
164
+ * 超采样倍数。
165
+ *
166
+ * 画布按「逻辑尺寸 × 该值」建立后在上下文上做等比 `scale`,缩回目标尺寸时由更多采样
167
+ * 得出边缘像素。清晰度主要取决于此:各版面最小字号仅 10~11px,1× 下一个字不足十几个
168
+ * 像素,JPEG 质量再高也无法还原。
169
+ *
170
+ * 3 在密集的封面与小字上收益仍可见;5 是收益递减的上限。
171
+ */
172
+ exports.DEFAULT_SUPERSAMPLE = 3;
173
+ /** 超采样倍数的可选上限,与配置项的 `.max()` 保持一致 */
174
+ exports.MAX_SUPERSAMPLE = 5;
175
+ /**
176
+ * 每日更新表的日期偏移范围(天)。
177
+ *
178
+ * 配置项的 `.min()/.max()` 与 `normalizeConfig` 的钳制共用这两个值——
179
+ * 两处分开写时极易只改一处,导致「配置页能填、实际被悄悄夹回去」。
180
+ */
181
+ exports.DAILY_OFFSET_MIN = -7;
182
+ exports.DAILY_OFFSET_MAX = 7;
151
183
  /** 注册到 canvas 的字体族名,与 config.render.fontFamily 的默认值保持一致 */
152
184
  exports.DEFAULT_FONT_FAMILY = 'Anime Schedule Sans';
153
185
  /**
@@ -162,37 +194,47 @@ exports.DEFAULT_FONT_FAMILY = 'Anime Schedule Sans';
162
194
  */
163
195
  exports.IMAGE_LAYOUT = {
164
196
  /** 统一外边距 */
165
- padding: 24,
197
+ padding: 28,
166
198
  /** 页眉 / 页脚固定高度 */
167
199
  headerHeight: 120,
168
200
  footerHeight: 52,
169
201
  /** 周历表:七天并排的画布宽度 */
170
202
  weekWidth: 1680,
171
- /** 单日时间轴(每日 / 深夜档)的画布宽度 */
172
- timelineWidth: 900,
203
+ /**
204
+ * 单日时间轴(每日 / 深夜档)画布宽度。
205
+ *
206
+ * 等于「左边距 + 时刻栏 + 轨道 + 时刻↔卡片间距 + 卡片宽 + 右边距」,不留余量,
207
+ * 使「现在」虚线的终点与卡片右缘重合。
208
+ */
209
+ timelineWidth: 798,
173
210
  /** 卡片列表(list 排版)的画布宽度 */
174
211
  listWidth: 900,
175
212
  /** 本季封面网格的画布宽度,要放得下 10 列 */
176
213
  seasonWidth: 1680,
177
214
  // ---- 通用行卡片 ----
178
- /** 行卡片高度(每日 / 列表排版)。容纳「标题 + 时刻 + 集数 + ID + 评分」五行 */
179
- rowHeight: 112,
215
+ /** 行卡片高度:容纳固定的两行标题 + 一行元信息 */
216
+ rowHeight: 104,
217
+ /** 行卡片宽度,窄于画布,避免文字仅占左半、右侧留空 */
218
+ rowCardWidth: 640,
180
219
  /** 行卡片之间的间距 */
181
220
  rowGap: 8,
182
221
  /** 行卡片圆角与内边距 */
183
222
  rowRadius: 12,
184
- rowPadding: 12,
223
+ rowPadding: 10,
185
224
  /** 行卡片封面高度,宽度按 1 : 1.4 推出 */
186
- mediaHeight: 82,
225
+ mediaHeight: 92,
187
226
  /** 封面与文字之间的间距 */
188
227
  mediaGap: 14,
189
- /** 卡片文字行间距。五行内容靠它撑开节奏,太小会挤成一团 */
228
+ /** 标题行高与固定行数:短标题也占两行,使元信息行在所有卡片上对齐 */
229
+ cardTitleLineHeight: 22,
230
+ cardTitleLines: 2,
231
+ /** 标题块与元信息行之间的间距 */
190
232
  cardLineGap: 6,
191
233
  /** 封面圆角 */
192
234
  mediaRadius: 6,
193
235
  // ---- 单日时间轴 ----
194
- /** 时间轴轨道相对左边距的位置 */
195
- timelineSpine: 50,
236
+ /** 左侧时刻栏宽度(内容左缘到轨道)。需容纳 `现在 18:30` 这类最长时刻文本,否则被画布左缘裁掉 */
237
+ timelineLabelWidth: 70,
196
238
  /** 轨道圆点半径 */
197
239
  timelineDot: 4,
198
240
  /** 时刻文字与轨道之间的间距 */
@@ -217,8 +259,7 @@ exports.IMAGE_LAYOUT = {
217
259
  /** 周历表条目内的封面高度(与三行文字齐高,保持 1 : 1.4 比例不再被压扁) */
218
260
  weekMediaHeight: 82,
219
261
  // ---- 卡片列表 ----
220
- /** 列表卡片高度(与行卡片一致,两种排版观感统一) */
221
- cardHeight: 112,
262
+ /** 列表卡片沿用行卡片高度,两种排版保持一致 */
222
263
  cardGap: 12,
223
264
  /** 两栏之间的间距 */
224
265
  cardColumnGap: 14,
@@ -247,17 +288,35 @@ exports.IMAGE_THEME = {
247
288
  surface: '#ffffff',
248
289
  cardBorder: '#e6e8ef',
249
290
  divider: '#eef0f5',
250
- /** 强调色:时刻、日期与「今天」高亮 */
251
- accent: '#f6665f',
291
+ /**
292
+ * 强调色:时刻、日期、「今天」高亮,以及详情卡的已播集数与峰值柱。
293
+ *
294
+ * 取珊瑚红 `#e26d6d`——比纯红柔和,整张表看下来不刺眼;详情卡的实心块共用它,
295
+ * 于是「已播集数」与列表里的「已订阅」气泡是同一套视觉语言。
296
+ */
297
+ accent: '#e26d6d',
252
298
  /** 封面的底色(等比完整显示时留白的那一圈) */
253
299
  badge: '#eef1f7',
300
+ /** 强调色的浅色底:软胶囊用 */
301
+ accentSoft: '#fdeceb',
302
+ /** 未播集数 / 禁用态的浅灰底 */
303
+ inert: '#f3f4f6',
304
+ /** 未播集数的文字色,比 muted 更浅,明确表示「还没到」 */
305
+ inertText: '#9ca3af',
306
+ /** 未播集数的描边:仅靠底色时与白色卡片难以区分,需勾出轮廓 */
307
+ inertBorder: '#e5e7eb',
308
+ /** 评分柱的非峰值色 */
309
+ scoreBar: '#fecaca',
254
310
  title: '#1f2330',
255
311
  body: '#5a6172',
256
312
  muted: '#9aa1b1',
257
313
  onAccent: '#ffffff',
258
- score: '#f0913c',
314
+ /** 评分:橙金色,全插件统一 */
315
+ score: '#e69a3a',
259
316
  /** 卡片投影:略深一点,白色卡片才从浅灰背景上立起来 */
260
317
  shadow: 'rgba(31, 35, 48, 0.10)',
318
+ /** 封面投影,比卡片投影更轻 */
319
+ coverShadow: 'rgba(0, 0, 0, 0.06)',
261
320
  };
262
321
  const ZH_CN = {
263
322
  weekdayNames: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
@@ -415,7 +474,7 @@ exports.RESPONSES = {
415
474
  databaseFailed: '数据库操作失败,请检查后台日志。',
416
475
  renderFailed: '图片渲染失败,已改用文本发送。',
417
476
  emptySearch: (keyword) => `未找到与 "${keyword}" 相关的作品,请尝试使用日文原名或更简短的关键词。`,
418
- notFound: '未找到该作品,它可能不在 bangumi-data 索引里。',
477
+ notFound: '未找到该作品,可能不在 bangumi-data 索引中。',
419
478
  };
420
479
  /** 日志里出现的固定短语 */
421
480
  exports.LOG_MESSAGES = {
@@ -1,16 +1,10 @@
1
1
  /**
2
- * 按标题把条目匹配到 AniList。
2
+ * 按标题把条目匹配到 AniList。用于运行时给「没带 `aniList` ID」的条目补 ID
3
+ * (`services/airing.ts`),以及维护脚本 `scripts/match-anilist.ts` 生成离线映射表。
3
4
  *
4
- * 用于两处:
5
- * - 运行时给「bangumi-data 没带 `aniList` ID」的条目补 ID(`services/airing.ts`);
6
- * - 维护脚本 `scripts/match-anilist.ts` 生成离线映射表。
7
- *
8
- * 判定标准分两级:
9
- * - **硬条件**:标题归一化后相等(含 AniList 的 `synonyms`)+ 首播日期相差不超过
10
- * `MAX_DAY_GAP` 天。前者排除无关条目,后者排除同名的不同季。
11
- * - **软条件**:播出时刻相差不超过 `MAX_HOUR_GAP` 小时。AniList 记的是「某一时刻可以看」,
12
- * 与日本电视档期本来就可能差几小时(配信早于电视),因此只用来提示「档期需订正」,
13
- * 不作为匹配是否成立的条件。
5
+ * **硬条件**(决定匹配是否成立):标题归一化后相等(含 `synonyms`)且首播日期相差不超过
6
+ * `MAX_DAY_GAP` 天。**软条件**:播出时刻相差不超过 `MAX_HOUR_GAP` 小时——AniList 记的是
7
+ * 「某一时刻可以看」,与电视档期可能差几小时,只用于提示「档期需订正」。
14
8
  */
15
9
  /** 首播日期允许的偏差(天) */
16
10
  export declare const MAX_DAY_GAP = 30;
@@ -18,9 +12,7 @@ export declare const MAX_DAY_GAP = 30;
18
12
  export declare const MAX_HOUR_GAP = 6;
19
13
  /**
20
14
  * 归一化标题:去空白与标点、全角转半角、罗马数字统一、去季度后缀。
21
- *
22
- * 目的是让「転生貴族、鑑定スキルで成り上がる(第3期)」与
23
- * 「転生貴族、鑑定スキルで成り上がる 第3期」这类写法差异不影响匹配。
15
+ * 目的是让「転生貴族、鑑定スキルで成り上がる(第3期)」与「… 第3期」这类写法差异不影响匹配。
24
16
  */
25
17
  export declare function normalizeTitle(value: unknown): string;
26
18
  /** 一条待匹配的作品信息 */
@@ -64,8 +56,7 @@ export interface TitleMatch {
64
56
  }
65
57
  /**
66
58
  * 在候选里挑出与目标最匹配的一个。
67
- *
68
59
  * 返回 null 表示没有候选满足硬条件——此时应当**放弃匹配**,退回周期规则的推算,
69
- * 而不是猜一个「看起来像」的。
60
+ * 而不是猜一个近似的候选。
70
61
  */
71
62
  export declare function matchByTitle(target: MatchTarget, candidates: MatchCandidate[]): TitleMatch | null;
@@ -1,17 +1,11 @@
1
1
  "use strict";
2
2
  /**
3
- * 按标题把条目匹配到 AniList。
3
+ * 按标题把条目匹配到 AniList。用于运行时给「没带 `aniList` ID」的条目补 ID
4
+ * (`services/airing.ts`),以及维护脚本 `scripts/match-anilist.ts` 生成离线映射表。
4
5
  *
5
- * 用于两处:
6
- * - 运行时给「bangumi-data 没带 `aniList` ID」的条目补 ID(`services/airing.ts`);
7
- * - 维护脚本 `scripts/match-anilist.ts` 生成离线映射表。
8
- *
9
- * 判定标准分两级:
10
- * - **硬条件**:标题归一化后相等(含 AniList 的 `synonyms`)+ 首播日期相差不超过
11
- * `MAX_DAY_GAP` 天。前者排除无关条目,后者排除同名的不同季。
12
- * - **软条件**:播出时刻相差不超过 `MAX_HOUR_GAP` 小时。AniList 记的是「某一时刻可以看」,
13
- * 与日本电视档期本来就可能差几小时(配信早于电视),因此只用来提示「档期需订正」,
14
- * 不作为匹配是否成立的条件。
6
+ * **硬条件**(决定匹配是否成立):标题归一化后相等(含 `synonyms`)且首播日期相差不超过
7
+ * `MAX_DAY_GAP` 天。**软条件**:播出时刻相差不超过 `MAX_HOUR_GAP` 小时——AniList 记的是
8
+ * 「某一时刻可以看」,与电视档期可能差几小时,只用于提示「档期需订正」。
15
9
  */
16
10
  Object.defineProperty(exports, "__esModule", { value: true });
17
11
  exports.MAX_HOUR_GAP = exports.MAX_DAY_GAP = void 0;
@@ -25,9 +19,7 @@ const HOUR = 3600000;
25
19
  const DAY = 86400000;
26
20
  /**
27
21
  * 归一化标题:去空白与标点、全角转半角、罗马数字统一、去季度后缀。
28
- *
29
- * 目的是让「転生貴族、鑑定スキルで成り上がる(第3期)」与
30
- * 「転生貴族、鑑定スキルで成り上がる 第3期」这类写法差异不影响匹配。
22
+ * 目的是让「転生貴族、鑑定スキルで成り上がる(第3期)」与「… 第3期」这类写法差异不影响匹配。
31
23
  */
32
24
  function normalizeTitle(value) {
33
25
  return String(value ?? '')
@@ -46,9 +38,8 @@ function startMsOf(candidate) {
46
38
  }
47
39
  /**
48
40
  * 在候选里挑出与目标最匹配的一个。
49
- *
50
41
  * 返回 null 表示没有候选满足硬条件——此时应当**放弃匹配**,退回周期规则的推算,
51
- * 而不是猜一个「看起来像」的。
42
+ * 而不是猜一个近似的候选。
52
43
  */
53
44
  function matchByTitle(target, candidates) {
54
45
  const wanted = normalizeTitle(target.title);
@@ -2,11 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BangumiDataLoader = void 0;
4
4
  /**
5
- * bangumi-data 的取数层。
6
- *
7
- * 数据来自 npm 包 `bangumi-data`(`main` 指向 `dist/data.json`),随插件一起安装、
8
- * 无需联网;只有包缺失时才依次回退到磁盘缓存与 CDN 镜像。
9
- *
5
+ * bangumi-data 的取数层。数据来自 npm 包 `bangumi-data`(随插件一起安装、无需联网),
6
+ * 只有包缺失时才依次回退到磁盘缓存与 CDN 镜像。
10
7
  * 这一层只负责「把 item 列表拿回来」,不解析、不建索引。
11
8
  */
12
9
  const fs_1 = require("fs");