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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ani-auto",
3
- "version": "1.4.3",
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
@@ -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
- normalizedPayload.photo = proxifyPosterUrl(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;
18
95
  }
19
96
 
20
- const endpoint = normalizedPayload.photo ? 'sendPhoto' : 'sendMessage';
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
- const imageUrl = proxifyPosterUrl(posterUrl);
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(imageUrl);
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
  };