ani-auto 1.4.3 → 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/package.json +1 -1
- package/src/anilist.js +5 -1
- package/src/engine.js +4 -2
- package/src/notify.js +82 -7
- package/utils.js +0 -17
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
|
@@ -3,10 +3,53 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const fetch = require('node-fetch');
|
|
5
5
|
const { loadConfig } = require('./config');
|
|
6
|
-
const { proxifyPosterUrl } = require('../utils');
|
|
7
6
|
|
|
8
7
|
const BOT_TOKEN = '8695132702:AAEFDoSL6nkdwDOoz5wmoVkmuhaYa4tNw0o';
|
|
9
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
|
+
|
|
10
53
|
async function sendTelegram(payload) {
|
|
11
54
|
const config = loadConfig();
|
|
12
55
|
const chatId = config.telegramChatId;
|
|
@@ -14,11 +57,44 @@ async function sendTelegram(payload) {
|
|
|
14
57
|
try {
|
|
15
58
|
const normalizedPayload = { ...payload };
|
|
16
59
|
if (normalizedPayload.photo) {
|
|
17
|
-
|
|
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;
|
|
18
95
|
}
|
|
19
96
|
|
|
20
|
-
const
|
|
21
|
-
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/${endpoint}`, {
|
|
97
|
+
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
|
22
98
|
method: 'POST',
|
|
23
99
|
headers: { 'Content-Type': 'application/json' },
|
|
24
100
|
body: JSON.stringify({ chat_id: String(chatId), parse_mode: 'HTML', ...normalizedPayload }),
|
|
@@ -46,14 +122,13 @@ async function sendTelegram(payload) {
|
|
|
46
122
|
async function sendDesktop(title, body, urgency = 'normal', posterUrl = null) {
|
|
47
123
|
try {
|
|
48
124
|
let icon = '';
|
|
49
|
-
|
|
50
|
-
if (imageUrl) {
|
|
125
|
+
if (posterUrl) {
|
|
51
126
|
const os = require('os');
|
|
52
127
|
const path = require('path');
|
|
53
128
|
const fs = require('fs');
|
|
54
129
|
const tmp = path.join(os.tmpdir(), 'ani-auto-poster.jpg');
|
|
55
130
|
try {
|
|
56
|
-
const res = await fetch(
|
|
131
|
+
const res = await fetch(posterUrl);
|
|
57
132
|
fs.writeFileSync(tmp, await res.buffer());
|
|
58
133
|
icon = `-i "${tmp}"`;
|
|
59
134
|
} catch (e) {}
|
package/utils.js
CHANGED
|
@@ -19,7 +19,6 @@ const config = loadConfig();
|
|
|
19
19
|
const API_DOMAIN = config.apiBaseUrl || 'https://aor-rex-anikuro-api.hf.space';
|
|
20
20
|
const SEARCH_API = `${API_DOMAIN}/api/anime/search`;
|
|
21
21
|
const RELEASES_API = `${API_DOMAIN}/api/anime`;
|
|
22
|
-
const IMAGE_PROXY_API = `${API_DOMAIN}/api/anime/image`;
|
|
23
22
|
const USER_AGENT = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36';
|
|
24
23
|
|
|
25
24
|
// --- Helpers ---
|
|
@@ -144,28 +143,12 @@ async function _fetchWithCookie(url, headers, resumeFrom) {
|
|
|
144
143
|
return fetch(url, { headers });
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
function proxifyPosterUrl(url) {
|
|
148
|
-
if (!url) return null;
|
|
149
|
-
try {
|
|
150
|
-
const parsed = new URL(url);
|
|
151
|
-
const host = parsed.hostname.toLowerCase();
|
|
152
|
-
if (host.includes('animepahe') || host.includes('kwik') || host.includes('pahe') || host.includes('uwucdn') || host.includes('owocdn') || host.includes('hakunaymatata')) {
|
|
153
|
-
return `${IMAGE_PROXY_API}?url=${encodeURIComponent(url)}`;
|
|
154
|
-
}
|
|
155
|
-
return url;
|
|
156
|
-
} catch {
|
|
157
|
-
return url;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
146
|
module.exports = {
|
|
162
147
|
SEARCH_API,
|
|
163
148
|
RELEASES_API,
|
|
164
|
-
IMAGE_PROXY_API,
|
|
165
149
|
USER_AGENT,
|
|
166
150
|
sleep,
|
|
167
151
|
getTerminalWidth,
|
|
168
152
|
formatBytes,
|
|
169
153
|
downloadFile,
|
|
170
|
-
proxifyPosterUrl,
|
|
171
154
|
};
|