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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install *)",
5
+ "Bash(npm cache *)"
6
+ ]
7
+ },
8
+ "$version": 3
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "automatic anime episode downloader",
5
5
  "bin": {
6
6
  "ani-auto": "src/cli.js"
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 = db.getAnimeBySiteTitle(workItem.title)?.poster_url || null;
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 = db.getAnimeBySiteTitle(workItem.title)?.poster_url || null;
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 endpoint = payload.photo ? 'sendPhoto' : 'sendMessage';
15
- await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/${endpoint}`, {
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', ...payload }),
100
+ body: JSON.stringify({ chat_id: String(chatId), parse_mode: 'HTML', ...normalizedPayload }),
19
101
  timeout: 5000,
20
102
  });
21
- } catch (e) {}
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 };