ani-auto 1.4.2 → 1.4.4
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/.qwen/settings.json +9 -0
- package/package.json +1 -1
- package/src/anilist.js +5 -1
- package/src/engine.js +4 -2
- package/src/notify.js +103 -5
package/package.json
CHANGED
package/src/anilist.js
CHANGED
|
@@ -15,6 +15,7 @@ query ($userName: String, $status: MediaListStatus) {
|
|
|
15
15
|
media {
|
|
16
16
|
id
|
|
17
17
|
title { romaji english native }
|
|
18
|
+
coverImage { extraLarge large medium }
|
|
18
19
|
episodes
|
|
19
20
|
status
|
|
20
21
|
nextAiringEpisode { episode airingAt }
|
|
@@ -35,6 +36,7 @@ query ($search: String) {
|
|
|
35
36
|
media(search: $search, type: ANIME) {
|
|
36
37
|
id
|
|
37
38
|
title { romaji english native }
|
|
39
|
+
coverImage { extraLarge large medium }
|
|
38
40
|
episodes
|
|
39
41
|
status
|
|
40
42
|
seasonYear
|
|
@@ -83,6 +85,7 @@ async function getUserList(userName, statuses, token = null) {
|
|
|
83
85
|
english: entry.media.title.english,
|
|
84
86
|
native: entry.media.title.native,
|
|
85
87
|
},
|
|
88
|
+
posterUrl: entry.media.coverImage?.extraLarge || entry.media.coverImage?.large || entry.media.coverImage?.medium || null,
|
|
86
89
|
nextAiring: entry.media.nextAiringEpisode || null,
|
|
87
90
|
});
|
|
88
91
|
}
|
|
@@ -104,6 +107,7 @@ async function searchAnime(query, token = null) {
|
|
|
104
107
|
status: m.status,
|
|
105
108
|
year: m.seasonYear,
|
|
106
109
|
format: m.format,
|
|
110
|
+
posterUrl: m.coverImage?.extraLarge || m.coverImage?.large || m.coverImage?.medium || null,
|
|
107
111
|
}));
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -111,4 +115,4 @@ function pickTitle(titles) {
|
|
|
111
115
|
return titles.english || titles.romaji || titles.native;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
module.exports = { getViewer, getUserList, searchAnime, pickTitle };
|
|
118
|
+
module.exports = { getViewer, getUserList, searchAnime, pickTitle };
|
package/src/engine.js
CHANGED
|
@@ -69,6 +69,7 @@ async function resolveWatchList(config) {
|
|
|
69
69
|
knownEpisodes: entry.totalEpisodes,
|
|
70
70
|
animeStatus: entry.animeStatus,
|
|
71
71
|
anilistStatus: entry.status,
|
|
72
|
+
posterUrl: entry.posterUrl || null,
|
|
72
73
|
nextAiringAt: entry.nextAiring?.airingAt || null,
|
|
73
74
|
});
|
|
74
75
|
}
|
|
@@ -90,6 +91,7 @@ async function resolveWatchList(config) {
|
|
|
90
91
|
knownEpisodes: null,
|
|
91
92
|
animeStatus: null,
|
|
92
93
|
anilistStatus: null,
|
|
94
|
+
posterUrl: null,
|
|
93
95
|
nextAiringAt: null,
|
|
94
96
|
});
|
|
95
97
|
}
|
|
@@ -326,7 +328,7 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
326
328
|
|
|
327
329
|
info(`[${workItem.title}] ${toDownload.length} unwatched episode(s) to download (Q:${q} L:${l}, skipping first ${watched}).`);
|
|
328
330
|
|
|
329
|
-
const posterUrl =
|
|
331
|
+
const posterUrl = workItem.posterUrl || null;
|
|
330
332
|
await notify.notifyDownloadStart(cleanTitle, toDownload.length, q, posterUrl);
|
|
331
333
|
|
|
332
334
|
const queue = [...toDownload];
|
|
@@ -364,7 +366,7 @@ async function processAnime(workItem, config, multiBar, auto = false, runQuality
|
|
|
364
366
|
|
|
365
367
|
db.markEpisodeStatus(dbRow.id, ep.episode, 'done', filename);
|
|
366
368
|
ok(`[${cleanTitle}] EP${ep.episode} → ${filename}`);
|
|
367
|
-
const posterUrl =
|
|
369
|
+
const posterUrl = workItem.posterUrl || null;
|
|
368
370
|
await notify.notifyEpisode(cleanTitle, relativeEp || ep.episode, q, posterUrl);
|
|
369
371
|
success = true;
|
|
370
372
|
results.downloaded++;
|
package/src/notify.js
CHANGED
|
@@ -6,19 +6,117 @@ const { loadConfig } = require('./config');
|
|
|
6
6
|
|
|
7
7
|
const BOT_TOKEN = '8695132702:AAEFDoSL6nkdwDOoz5wmoVkmuhaYa4tNw0o';
|
|
8
8
|
|
|
9
|
+
async function fetchPosterBuffer(photoUrl) {
|
|
10
|
+
if (!photoUrl) return null;
|
|
11
|
+
const res = await fetch(photoUrl, { timeout: 10000 });
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
throw new Error(`Poster fetch failed: HTTP ${res.status}`);
|
|
14
|
+
}
|
|
15
|
+
const contentType = res.headers.get('content-type') || 'image/jpeg';
|
|
16
|
+
const buffer = await res.buffer();
|
|
17
|
+
if (!buffer || !buffer.length) {
|
|
18
|
+
throw new Error('Poster fetch returned empty body');
|
|
19
|
+
}
|
|
20
|
+
return { buffer, contentType };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildMultipartBody(fields, fileField) {
|
|
24
|
+
const boundary = `----ani-auto-${Date.now().toString(16)}`;
|
|
25
|
+
const chunks = [];
|
|
26
|
+
|
|
27
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
28
|
+
chunks.push(Buffer.from(
|
|
29
|
+
`--${boundary}\r\n` +
|
|
30
|
+
`Content-Disposition: form-data; name="${key}"\r\n\r\n` +
|
|
31
|
+
`${value}\r\n`
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (fileField) {
|
|
36
|
+
chunks.push(Buffer.from(
|
|
37
|
+
`--${boundary}\r\n` +
|
|
38
|
+
`Content-Disposition: form-data; name="${fileField.name}"; filename="${fileField.filename}"\r\n` +
|
|
39
|
+
`Content-Type: ${fileField.contentType}\r\n\r\n`
|
|
40
|
+
));
|
|
41
|
+
chunks.push(fileField.buffer);
|
|
42
|
+
chunks.push(Buffer.from('\r\n'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
body: Buffer.concat(chunks),
|
|
49
|
+
boundary,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
9
53
|
async function sendTelegram(payload) {
|
|
10
54
|
const config = loadConfig();
|
|
11
55
|
const chatId = config.telegramChatId;
|
|
12
56
|
if (!chatId || config.telegramNotify === false) return;
|
|
13
57
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
58
|
+
const normalizedPayload = { ...payload };
|
|
59
|
+
if (normalizedPayload.photo) {
|
|
60
|
+
const poster = await fetchPosterBuffer(normalizedPayload.photo);
|
|
61
|
+
const { body, boundary } = buildMultipartBody(
|
|
62
|
+
{
|
|
63
|
+
chat_id: String(chatId),
|
|
64
|
+
parse_mode: 'HTML',
|
|
65
|
+
caption: normalizedPayload.caption || '',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'photo',
|
|
69
|
+
filename: 'poster.jpg',
|
|
70
|
+
contentType: poster.contentType,
|
|
71
|
+
buffer: poster.buffer,
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendPhoto`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
|
|
77
|
+
body,
|
|
78
|
+
timeout: 15000,
|
|
79
|
+
});
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(`Telegram API ${res.status}: ${text.slice(0, 200)}`);
|
|
83
|
+
}
|
|
84
|
+
if (text) {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(text);
|
|
87
|
+
if (data.ok === false) {
|
|
88
|
+
throw new Error(`Telegram API rejected message: ${data.description || 'unknown error'}`);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.message.startsWith('Telegram API rejected message:')) throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
|
16
98
|
method: 'POST',
|
|
17
99
|
headers: { 'Content-Type': 'application/json' },
|
|
18
|
-
body: JSON.stringify({ chat_id: String(chatId), parse_mode: 'HTML', ...
|
|
100
|
+
body: JSON.stringify({ chat_id: String(chatId), parse_mode: 'HTML', ...normalizedPayload }),
|
|
19
101
|
timeout: 5000,
|
|
20
102
|
});
|
|
21
|
-
|
|
103
|
+
const body = await res.text();
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(`Telegram API ${res.status}: ${body.slice(0, 200)}`);
|
|
106
|
+
}
|
|
107
|
+
if (body) {
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(body);
|
|
110
|
+
if (data.ok === false) {
|
|
111
|
+
throw new Error(`Telegram API rejected message: ${data.description || 'unknown error'}`);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err.message.startsWith('Telegram API rejected message:')) throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.warn(`[notify] Telegram send failed: ${e.message}`);
|
|
119
|
+
}
|
|
22
120
|
}
|
|
23
121
|
|
|
24
122
|
async function sendDesktop(title, body, urgency = 'normal', posterUrl = null) {
|
|
@@ -118,4 +216,4 @@ async function notifyDaemonStop() {
|
|
|
118
216
|
]);
|
|
119
217
|
}
|
|
120
218
|
|
|
121
|
-
module.exports = { notifyDownloadStart, notifyEpisode, notifyFailed, notifySummary, notifyDaemonStart, notifyDaemonStop };
|
|
219
|
+
module.exports = { notifyDownloadStart, notifyEpisode, notifyFailed, notifySummary, notifyDaemonStart, notifyDaemonStop };
|