ani-auto 1.4.3 → 2.0.0
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/anime/LICENSE +21 -0
- package/anime/api/index.js +110 -0
- package/anime/app.js +79 -0
- package/anime/controllers/animeInfoController.js +48 -0
- package/anime/controllers/animeListController.js +21 -0
- package/anime/controllers/homeController.js +42 -0
- package/anime/controllers/playController.js +140 -0
- package/anime/controllers/queueController.js +20 -0
- package/anime/controllers/testController.js +667 -0
- package/anime/index.js +139 -0
- package/anime/middleware/cache.js +64 -0
- package/anime/middleware/errorHandler.js +33 -0
- package/anime/middleware/rateLimiter.js +73 -0
- package/anime/models/animeInfoModel.js +193 -0
- package/anime/models/animeListModel.js +69 -0
- package/anime/models/homeModel.js +33 -0
- package/anime/models/playModel.js +528 -0
- package/anime/models/queueModel.js +22 -0
- package/anime/package.json +27 -0
- package/anime/pnpm-lock.yaml +1774 -0
- package/anime/readme.md +236 -0
- package/anime/routes/animeInfoRoutes.js +9 -0
- package/anime/routes/animeListRoutes.js +9 -0
- package/anime/routes/homeRoutes.js +10 -0
- package/anime/routes/playRoutes.js +11 -0
- package/anime/routes/queueRoutes.js +8 -0
- package/anime/routes/testRoutes.js +325 -0
- package/anime/routes/webhookRoutes.js +23 -0
- package/anime/scrapers/animepahe.js +1053 -0
- package/anime/test-server.js +10 -0
- package/anime/test.sh +275 -0
- package/anime/utils/browser.js +172 -0
- package/anime/utils/config.js +209 -0
- package/anime/utils/dataProcessor.js +172 -0
- package/anime/utils/diskCache.js +228 -0
- package/anime/utils/flaresolverr.js +361 -0
- package/anime/utils/jsParser.js +19 -0
- package/anime/utils/redis.js +64 -0
- package/anime/utils/requestManager.js +706 -0
- package/anime/utils/urlConverter.js +88 -0
- package/anime/utils/webhookNotifier.js +190 -0
- package/cookiereader.py +291 -0
- package/package.json +14 -6
- package/src/anilist.js +19 -2
- package/src/api/anilist.js +32 -0
- package/src/api/anime.js +181 -0
- package/src/api/cf-bypass.js +131 -0
- package/src/api/config.js +134 -0
- package/src/api/daemon.js +62 -0
- package/src/api/download.js +98 -0
- package/src/api/match.js +58 -0
- package/src/api/runs.js +15 -0
- package/src/api/update.js +96 -0
- package/src/cli.js +18 -2
- package/src/config.js +14 -2
- package/src/daemon.js +1 -1
- package/src/db.js +38 -11
- package/src/engine.js +66 -13
- package/src/notify.js +82 -7
- package/src/scraper.js +2 -2
- package/src/server.js +108 -0
- package/utils.js +21 -21
- package/web/index.html +13 -0
- package/web/package-lock.json +1420 -0
- package/web/package.json +24 -0
- package/web/src/App.vue +200 -0
- package/web/src/api/client.js +75 -0
- package/web/src/assets/styles/base.css +961 -0
- package/web/src/components/UpdateModal.vue +157 -0
- package/web/src/components/sidebar/SidebarResizable.vue +170 -0
- package/web/src/components/sidebar/sidebarConfig.js +24 -0
- package/web/src/main.js +104 -0
- package/web/src/router/index.js +44 -0
- package/web/src/stores/config.js +27 -0
- package/web/src/stores/download.js +107 -0
- package/web/src/stores/update.js +73 -0
- package/web/src/views/About.vue +42 -0
- package/web/src/views/AniListSync.vue +122 -0
- package/web/src/views/AnimeDetail.vue +202 -0
- package/web/src/views/AnimeList.vue +110 -0
- package/web/src/views/CFResult.vue +312 -0
- package/web/src/views/DaemonControl.vue +83 -0
- package/web/src/views/Dashboard.vue +187 -0
- package/web/src/views/DownloadCenter.vue +153 -0
- package/web/src/views/MatchFinder.vue +131 -0
- package/web/src/views/OAuthCallback.vue +44 -0
- package/web/src/views/Onboarding.vue +93 -0
- package/web/src/views/RunHistory.vue +122 -0
- package/web/src/views/Settings.vue +410 -0
- package/web/vite.config.js +23 -0
package/src/api/anime.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const db = require('../db');
|
|
5
|
+
const { loadConfig, updateConfig } = require('../config');
|
|
6
|
+
const { searchAnime } = require('../anilist');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
async function init(req, res, next) {
|
|
11
|
+
await db.initDb();
|
|
12
|
+
next();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
router.use(init);
|
|
16
|
+
|
|
17
|
+
router.get('/', (req, res) => {
|
|
18
|
+
const rawDb = db.getDb();
|
|
19
|
+
const stmt = rawDb.prepare('SELECT * FROM anime ORDER BY title');
|
|
20
|
+
const all = [];
|
|
21
|
+
while (stmt.step()) all.push(stmt.getAsObject());
|
|
22
|
+
stmt.free();
|
|
23
|
+
const withStats = all.map((a) => ({
|
|
24
|
+
...a,
|
|
25
|
+
stats: db.getEpisodeStats(a.id),
|
|
26
|
+
}));
|
|
27
|
+
res.json(withStats);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
router.get('/:id', (req, res) => {
|
|
31
|
+
const rawDb = db.getDb();
|
|
32
|
+
const stmt = rawDb.prepare('SELECT * FROM anime WHERE id = ?');
|
|
33
|
+
stmt.bind([req.params.id]);
|
|
34
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
35
|
+
stmt.free();
|
|
36
|
+
if (!row) return res.status(404).json({ error: 'not found' });
|
|
37
|
+
row.stats = db.getEpisodeStats(row.id);
|
|
38
|
+
res.json(row);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.get('/:id/episodes', (req, res) => {
|
|
42
|
+
const episodes = db.getAllEpisodes(req.params.id);
|
|
43
|
+
res.json(episodes);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
router.post('/', async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const { title, anilistId, quality, language, poster_url } = req.body;
|
|
49
|
+
if (!title) return res.status(400).json({ error: 'title required' });
|
|
50
|
+
|
|
51
|
+
let resolvedTitle = title;
|
|
52
|
+
let resolvedAnilistId = anilistId || null;
|
|
53
|
+
|
|
54
|
+
if (!anilistId) {
|
|
55
|
+
const results = await searchAnime(title);
|
|
56
|
+
if (results.length) {
|
|
57
|
+
resolvedAnilistId = results[0].anilistId;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const config = loadConfig();
|
|
62
|
+
const manualList = config.manualList || [];
|
|
63
|
+
const exists = manualList.find((m) => m.title.toLowerCase() === resolvedTitle.toLowerCase());
|
|
64
|
+
if (exists) {
|
|
65
|
+
return res.json({ ok: true, message: 'already in list' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entry = {
|
|
69
|
+
title: resolvedTitle,
|
|
70
|
+
anilistId: resolvedAnilistId,
|
|
71
|
+
enabled: true,
|
|
72
|
+
quality: quality || null,
|
|
73
|
+
language: language || null,
|
|
74
|
+
poster_url: poster_url || null,
|
|
75
|
+
};
|
|
76
|
+
manualList.push(entry);
|
|
77
|
+
updateConfig({ manualList });
|
|
78
|
+
|
|
79
|
+
db.upsertAnime({
|
|
80
|
+
title: resolvedTitle,
|
|
81
|
+
siteId: resolvedTitle,
|
|
82
|
+
anilistId: resolvedAnilistId,
|
|
83
|
+
source: 'manual',
|
|
84
|
+
quality: quality || null,
|
|
85
|
+
language: language || null,
|
|
86
|
+
poster_url: poster_url || null,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
res.json({ ok: true, entry });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
res.status(500).json({ error: e.message });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
router.delete('/:id', (req, res) => {
|
|
96
|
+
const rawDb = db.getDb();
|
|
97
|
+
const stmt = rawDb.prepare('SELECT * FROM anime WHERE id = ?');
|
|
98
|
+
stmt.bind([req.params.id]);
|
|
99
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
100
|
+
stmt.free();
|
|
101
|
+
if (!row) return res.status(404).json({ error: 'not found' });
|
|
102
|
+
|
|
103
|
+
db.removeAnime(row.site_id);
|
|
104
|
+
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
const manualList = (config.manualList || []).filter(
|
|
107
|
+
(m) => m.title.toLowerCase() !== (row.anilist_title || row.title).toLowerCase()
|
|
108
|
+
);
|
|
109
|
+
updateConfig({ manualList });
|
|
110
|
+
|
|
111
|
+
res.json({ ok: true });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
router.put('/:id/enable', (req, res) => {
|
|
115
|
+
const enabled = req.body.enabled !== false;
|
|
116
|
+
const rawDb = db.getDb();
|
|
117
|
+
const stmt = rawDb.prepare('SELECT * FROM anime WHERE id = ?');
|
|
118
|
+
stmt.bind([req.params.id]);
|
|
119
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
120
|
+
stmt.free();
|
|
121
|
+
if (!row) return res.status(404).json({ error: 'not found' });
|
|
122
|
+
db.setAnimeEnabled(row.site_id, enabled);
|
|
123
|
+
res.json({ ok: true, enabled });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
router.put('/:id/quality', (req, res) => {
|
|
127
|
+
const quality = req.body.quality || null;
|
|
128
|
+
const rawDb = db.getDb();
|
|
129
|
+
const stmt = rawDb.prepare('SELECT * FROM anime WHERE id = ?');
|
|
130
|
+
stmt.bind([req.params.id]);
|
|
131
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
132
|
+
stmt.free();
|
|
133
|
+
if (!row) return res.status(404).json({ error: 'not found' });
|
|
134
|
+
db.setAnimeQuality(row.anilist_title || row.title, quality);
|
|
135
|
+
res.json({ ok: true, quality });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
router.put('/:id/language', (req, res) => {
|
|
139
|
+
const language = req.body.language || null;
|
|
140
|
+
const rawDb = db.getDb();
|
|
141
|
+
const stmt = rawDb.prepare('SELECT * FROM anime WHERE id = ?');
|
|
142
|
+
stmt.bind([req.params.id]);
|
|
143
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
144
|
+
stmt.free();
|
|
145
|
+
if (!row) return res.status(404).json({ error: 'not found' });
|
|
146
|
+
db.setAnimeQuality(row.anilist_title || row.title, language);
|
|
147
|
+
res.json({ ok: true, language });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
router.post('/refresh-posters', async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
await db.initDb();
|
|
153
|
+
const rawDb = db.getDb();
|
|
154
|
+
const stmt = rawDb.prepare("SELECT * FROM anime WHERE poster_url IS NULL AND anilist_id IS NOT NULL");
|
|
155
|
+
const rows = [];
|
|
156
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
157
|
+
stmt.free();
|
|
158
|
+
const { getMediaById } = require('../anilist');
|
|
159
|
+
const config = loadConfig();
|
|
160
|
+
let updated = 0;
|
|
161
|
+
for (const row of rows) {
|
|
162
|
+
try {
|
|
163
|
+
const media = await getMediaById(row.anilist_id, config.anilistToken || null);
|
|
164
|
+
if (media?.coverImage) {
|
|
165
|
+
const url = media.coverImage.extraLarge || media.coverImage.large || media.coverImage.medium;
|
|
166
|
+
if (url) {
|
|
167
|
+
db.execSql('UPDATE anime SET poster_url = ? WHERE id = ?', [url, row.id]);
|
|
168
|
+
updated++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.warn('failed to fetch poster for', row.anilist_id, e.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
res.json({ ok: true, updated, total: rows.length });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
res.status(500).json({ error: e.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
module.exports = router;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const { loadConfig, updateConfig } = require('../config');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
const BYPASS_URL = 'https://animepahe.pw';
|
|
11
|
+
const COOKIE_READER = path.join(__dirname, '..', '..', 'cookiereader.py');
|
|
12
|
+
|
|
13
|
+
function runCookiereader(...args) {
|
|
14
|
+
try {
|
|
15
|
+
const stdout = execSync(`python3 "${COOKIE_READER}" ${args.join(' ')}`, {
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
timeout: 15000,
|
|
18
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
19
|
+
});
|
|
20
|
+
return JSON.parse(stdout.trim());
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { error: e.message };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function verifyWithCurl(cfClearance, userAgent) {
|
|
27
|
+
try {
|
|
28
|
+
const testUrl = 'https://animepahe.pw/api?m=search&q=Naruto&page=1';
|
|
29
|
+
const args = ['-sS', '-L', '--compressed'];
|
|
30
|
+
args.push('-b', `cf_clearance=${cfClearance}`);
|
|
31
|
+
if (userAgent) args.push('-A', userAgent);
|
|
32
|
+
args.push('-w', '\n%{http_code}');
|
|
33
|
+
args.push(testUrl);
|
|
34
|
+
|
|
35
|
+
const result = spawnSync('curl', args, { encoding: 'utf8', timeout: 15000 });
|
|
36
|
+
if (result.error) throw result.error;
|
|
37
|
+
const lines = result.stdout.trim().split('\n');
|
|
38
|
+
const statusCode = parseInt(lines.pop(), 10);
|
|
39
|
+
const body = lines.join('\n');
|
|
40
|
+
|
|
41
|
+
if (statusCode === 403 || body.includes('Just a moment') || body.includes('DDoS-GUARD')) {
|
|
42
|
+
return { ok: false, error: 'Cookies rejected by Cloudflare' };
|
|
43
|
+
}
|
|
44
|
+
if (statusCode !== 200) {
|
|
45
|
+
return { ok: false, error: `Unexpected HTTP ${statusCode}` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true };
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return { ok: false, error: e.message };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
router.post('/cf-bypass/detect', async (req, res) => {
|
|
54
|
+
const result = runCookiereader('detect');
|
|
55
|
+
if (result.error) {
|
|
56
|
+
return res.status(500).json({ ok: false, error: result.error });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const found = {};
|
|
60
|
+
for (const [browser, data] of Object.entries(result)) {
|
|
61
|
+
if (data.found) {
|
|
62
|
+
found[browser] = {
|
|
63
|
+
found: true,
|
|
64
|
+
cookie_string: data.cookie_string,
|
|
65
|
+
};
|
|
66
|
+
} else if (data.error) {
|
|
67
|
+
found[browser] = { found: false, error: data.error };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
res.json({ ok: true, browsers: result, found });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
router.post('/cf-bypass/save', async (req, res) => {
|
|
75
|
+
const { cfClearance, userAgent } = req.body || {};
|
|
76
|
+
if (!cfClearance) {
|
|
77
|
+
return res.status(400).json({ ok: false, error: 'cf_clearance value is required' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ua = userAgent || 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0';
|
|
81
|
+
|
|
82
|
+
const verifyResult = verifyWithCurl(cfClearance, ua);
|
|
83
|
+
if (!verifyResult.ok) {
|
|
84
|
+
return res.status(400).json({
|
|
85
|
+
ok: false,
|
|
86
|
+
error: verifyResult.error || 'Verification failed',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
updateConfig({
|
|
91
|
+
animepaheCfClearance: cfClearance,
|
|
92
|
+
animepaheUserAgent: ua,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const PaheConfig = require('../../anime/utils/config');
|
|
97
|
+
PaheConfig.setCfClearance(cfClearance);
|
|
98
|
+
PaheConfig.userAgent = ua;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.warn('[cf-bypass] Could not update PaheConfig:', e.message);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`[cf-bypass] cf_clearance saved and verified`);
|
|
104
|
+
res.json({ ok: true, message: 'Saved and verified' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.get('/cf-bypass/status', (req, res) => {
|
|
108
|
+
const config = loadConfig();
|
|
109
|
+
res.json({
|
|
110
|
+
bypassed: !!config.animepaheCfClearance,
|
|
111
|
+
userAgent: config.animepaheUserAgent || null,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
router.delete('/cf-bypass', (req, res) => {
|
|
116
|
+
updateConfig({
|
|
117
|
+
animepaheCfClearance: null,
|
|
118
|
+
animepaheUserAgent: null,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const PaheConfig = require('../../anime/utils/config');
|
|
123
|
+
PaheConfig.setCfClearance('');
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn('[cf-bypass] Could not clear PaheConfig:', e.message);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
res.json({ ok: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
module.exports = router;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const fetch = require('node-fetch');
|
|
5
|
+
const { loadConfig, updateConfig } = require('../config');
|
|
6
|
+
const { getViewer } = require('../anilist');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
router.get('/', async (req, res) => {
|
|
11
|
+
const config = { ...loadConfig() };
|
|
12
|
+
if (config.anilistToken) {
|
|
13
|
+
if (!config.anilistAvatar) {
|
|
14
|
+
try {
|
|
15
|
+
const viewer = await getViewer(config.anilistToken);
|
|
16
|
+
if (viewer?.avatar?.medium) {
|
|
17
|
+
config.anilistAvatar = viewer.avatar.medium;
|
|
18
|
+
updateConfig({ anilistAvatar: viewer.avatar.medium });
|
|
19
|
+
}
|
|
20
|
+
} catch (e) { console.warn('failed to fetch anilist avatar', e); }
|
|
21
|
+
}
|
|
22
|
+
config.anilistToken = '[set]';
|
|
23
|
+
}
|
|
24
|
+
if (config.anilistClientSecret) config.anilistClientSecret = '[set]';
|
|
25
|
+
if (config.animepaheCfClearance) config.animepaheCfClearance = '[set]';
|
|
26
|
+
if (config.animepaheUserAgent) config.animepaheUserAgent = '[set]';
|
|
27
|
+
res.json(config);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
router.put('/', (req, res) => {
|
|
31
|
+
const updates = {};
|
|
32
|
+
const allowed = [
|
|
33
|
+
'anilistUsername', 'anilistToken', 'anilistAvatar', 'watchStatuses',
|
|
34
|
+
'quality', 'language', 'concurrency', 'outputDir', 'maxRetries',
|
|
35
|
+
'daemonIntervalMinutes', 'airingBufferMinutes', 'daemonEnabled',
|
|
36
|
+
'autoUpdateCheck',
|
|
37
|
+
'telegramChatId', 'telegramNotify',
|
|
38
|
+
'manualList', 'onboardingSkipped',
|
|
39
|
+
'anilistClientId', 'anilistClientSecret',
|
|
40
|
+
];
|
|
41
|
+
for (const key of allowed) {
|
|
42
|
+
if (req.body[key] !== undefined) updates[key] = req.body[key];
|
|
43
|
+
}
|
|
44
|
+
const updated = updateConfig(updates);
|
|
45
|
+
if (updated.anilistToken) updated.anilistToken = '[set]';
|
|
46
|
+
if (updated.anilistClientSecret) updated.anilistClientSecret = '[set]';
|
|
47
|
+
if (updated.animepaheCfClearance) updated.animepaheCfClearance = '[set]';
|
|
48
|
+
if (updated.animepaheUserAgent) updated.animepaheUserAgent = '[set]';
|
|
49
|
+
res.json(updated);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
router.get('/oauth-url', (req, res) => {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const hasSecret = !!config.anilistClientSecret;
|
|
55
|
+
|
|
56
|
+
if (hasSecret) {
|
|
57
|
+
const base = req.protocol + '://' + req.get('host');
|
|
58
|
+
const callbackUrl = base + '/api/config/oauth-callback';
|
|
59
|
+
const url = 'https://anilist.co/api/v2/oauth/authorize'
|
|
60
|
+
+ '?client_id=' + config.anilistClientId
|
|
61
|
+
+ '&redirect_uri=' + encodeURIComponent(callbackUrl)
|
|
62
|
+
+ '&response_type=code';
|
|
63
|
+
res.json({ url });
|
|
64
|
+
} else {
|
|
65
|
+
res.json({
|
|
66
|
+
url: 'https://anilist.co/api/v2/oauth/authorize?client_id=37183&response_type=token',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
router.get('/oauth-callback', async (req, res) => {
|
|
72
|
+
const code = req.query.code;
|
|
73
|
+
if (!code) {
|
|
74
|
+
return res.type('html').send(`<!DOCTYPE html>
|
|
75
|
+
<html><head><meta charset="utf-8"><title>Redirect...</title></head>
|
|
76
|
+
<body><script>window.location.replace('/oauth/callback'+window.location.hash)</script></body></html>`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const config = loadConfig();
|
|
80
|
+
const clientId = config.anilistClientId || '37183';
|
|
81
|
+
const clientSecret = config.anilistClientSecret;
|
|
82
|
+
if (!clientSecret) {
|
|
83
|
+
return res.status(400).send('No AniList client secret configured');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const base = req.protocol + '://' + req.get('host');
|
|
87
|
+
const callbackUrl = base + '/api/config/oauth-callback';
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const tokenRes = await fetch('https://anilist.co/api/v2/oauth/token', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
grant_type: 'authorization_code',
|
|
95
|
+
client_id: clientId,
|
|
96
|
+
client_secret: clientSecret,
|
|
97
|
+
redirect_uri: callbackUrl,
|
|
98
|
+
code,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!tokenRes.ok) {
|
|
103
|
+
const text = await tokenRes.text();
|
|
104
|
+
console.error('token exchange failed', tokenRes.status, text);
|
|
105
|
+
return res.redirect(base + '/oauth/callback#error=token_exchange_failed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const tokenData = await tokenRes.json();
|
|
109
|
+
const token = tokenData.access_token;
|
|
110
|
+
|
|
111
|
+
const viewer = await getViewer(token);
|
|
112
|
+
updateConfig({
|
|
113
|
+
anilistToken: token,
|
|
114
|
+
anilistUsername: viewer.name,
|
|
115
|
+
anilistAvatar: viewer.avatar?.medium || null,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
res.redirect(base + '/oauth/callback#access_token=' + encodeURIComponent(token));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error('oauth callback error', e);
|
|
121
|
+
res.redirect(base + '/oauth/callback#error=' + encodeURIComponent(e.message));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
router.post('/verify-token', async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const viewer = await getViewer(req.body.token);
|
|
128
|
+
res.json({ ok: true, name: viewer.name, avatar: viewer.avatar?.medium || null });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
res.json({ ok: false, error: e.message });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
module.exports = router;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const { spawnSync, spawn } = require('child_process');
|
|
5
|
+
const { loadConfig, updateConfig } = require('../config');
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
const SERVICE = 'ani-auto';
|
|
9
|
+
|
|
10
|
+
function run(cmd) {
|
|
11
|
+
try {
|
|
12
|
+
const r = spawnSync(cmd, { shell: true, encoding: 'utf8', timeout: 10000 });
|
|
13
|
+
return { out: r.stdout?.trim(), err: r.stderr?.trim(), code: r.status };
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return { out: '', err: e.message, code: 1 };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
router.get('/status', (req, res) => {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const enabled = config.daemonEnabled !== false;
|
|
22
|
+
const r = run(`systemctl --user status ${SERVICE}`);
|
|
23
|
+
const running = r.out?.includes('active (running)');
|
|
24
|
+
res.json({ enabled, running, output: r.out || r.err });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
router.post('/start', (req, res) => {
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
if (config.daemonEnabled === false) {
|
|
30
|
+
return res.json({ ok: false, error: 'daemon is disabled' });
|
|
31
|
+
}
|
|
32
|
+
const r = run(`systemctl --user start ${SERVICE}`);
|
|
33
|
+
res.json({ ok: r.code === 0, error: r.err || null });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
router.post('/stop', (req, res) => {
|
|
37
|
+
const r = run(`systemctl --user stop ${SERVICE}`);
|
|
38
|
+
res.json({ ok: r.code === 0, error: r.err || null });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.post('/restart', (req, res) => {
|
|
42
|
+
const config = loadConfig();
|
|
43
|
+
if (config.daemonEnabled === false) {
|
|
44
|
+
return res.json({ ok: false, error: 'daemon is disabled' });
|
|
45
|
+
}
|
|
46
|
+
const r = run(`systemctl --user restart ${SERVICE}`);
|
|
47
|
+
res.json({ ok: r.code === 0, error: r.err || null });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.post('/enable', (req, res) => {
|
|
51
|
+
updateConfig({ daemonEnabled: true });
|
|
52
|
+
run(`systemctl --user start ${SERVICE}`);
|
|
53
|
+
res.json({ ok: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
router.post('/disable', (req, res) => {
|
|
57
|
+
updateConfig({ daemonEnabled: false });
|
|
58
|
+
run(`systemctl --user stop ${SERVICE}`);
|
|
59
|
+
res.json({ ok: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
module.exports = router;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const { loadConfig } = require('../config');
|
|
5
|
+
const db = require('../db');
|
|
6
|
+
const { runCycle } = require('../engine');
|
|
7
|
+
const scraper = require('../scraper');
|
|
8
|
+
const { EventEmitter } = require('events');
|
|
9
|
+
|
|
10
|
+
module.exports = function createDownloadRouter(downloadBus) {
|
|
11
|
+
const router = Router();
|
|
12
|
+
|
|
13
|
+
router.post('/run', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const quality = req.body.quality || null;
|
|
16
|
+
const language = req.body.language || null;
|
|
17
|
+
const statuses = req.body.statuses || null;
|
|
18
|
+
runCycle({ auto: true, quality, language, statuses, eventBus: downloadBus })
|
|
19
|
+
.catch((e) => {
|
|
20
|
+
downloadBus.emit('broadcast', 'run:error', { error: e.message });
|
|
21
|
+
});
|
|
22
|
+
res.json({ ok: true, message: 'Download cycle started' });
|
|
23
|
+
} catch (e) {
|
|
24
|
+
res.status(500).json({ error: e.message });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
router.post('/anime/:id', async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
await db.initDb();
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
const rawDb = db.getDb();
|
|
33
|
+
const stmt = rawDb.prepare('SELECT * FROM anime WHERE id = ?');
|
|
34
|
+
stmt.bind([req.params.id]);
|
|
35
|
+
const row = stmt.step() ? stmt.getAsObject() : null;
|
|
36
|
+
stmt.free();
|
|
37
|
+
if (!row) return res.status(404).json({ error: 'anime not found' });
|
|
38
|
+
|
|
39
|
+
const watchItem = {
|
|
40
|
+
title: row.anilist_title || row.title,
|
|
41
|
+
anilistId: row.anilist_id,
|
|
42
|
+
source: row.source,
|
|
43
|
+
quality: row.override_quality || row.quality || null,
|
|
44
|
+
language: row.language || null,
|
|
45
|
+
watchedEpisodes: 0,
|
|
46
|
+
knownEpisodes: null,
|
|
47
|
+
posterUrl: row.poster_url || null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const { processAnime } = require('../engine');
|
|
51
|
+
processAnime(watchItem, config, null, true, req.body.quality || null, req.body.language || null, null, downloadBus)
|
|
52
|
+
.then((result) => {
|
|
53
|
+
downloadBus.emit('broadcast', 'anime:complete', { animeId: req.params.id, ...result });
|
|
54
|
+
downloadBus.emit('broadcast', 'run:complete', { downloaded: result?.downloaded || 0, skipped: result?.skipped || 0, failed: result?.failed || 0 });
|
|
55
|
+
})
|
|
56
|
+
.catch((e) => {
|
|
57
|
+
downloadBus.emit('broadcast', 'anime:error', { animeId: req.params.id, error: e.message });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
res.json({ ok: true, message: `Download started for ${watchItem.title}` });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
res.status(500).json({ error: e.message });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
router.post('/retry', async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
await db.initDb();
|
|
69
|
+
const failed = db.getFailedEpisodes();
|
|
70
|
+
if (!failed.length) return res.json({ ok: true, message: 'no failed episodes' });
|
|
71
|
+
|
|
72
|
+
const byAnime = {};
|
|
73
|
+
for (const ep of failed) {
|
|
74
|
+
const key = ep.anilist_title || ep.title;
|
|
75
|
+
if (!byAnime[key]) byAnime[key] = [];
|
|
76
|
+
byAnime[key].push(ep);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const [title, eps] of Object.entries(byAnime)) {
|
|
80
|
+
for (const ep of eps) {
|
|
81
|
+
db.markEpisodeStatus(ep.anime_id, ep.episode_num, 'pending', null, null);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const config = loadConfig();
|
|
86
|
+
runCycle({ auto: true, quality: null, language: null, statuses: null, eventBus: downloadBus })
|
|
87
|
+
.catch((e) => {
|
|
88
|
+
downloadBus.emit('broadcast', 'run:error', { error: e.message });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
res.json({ ok: true, message: `Retrying ${failed.length} failed episode(s)` });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
res.status(500).json({ error: e.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return router;
|
|
98
|
+
};
|
package/src/api/match.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const db = require('../db');
|
|
5
|
+
const scraper = require('../scraper');
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
|
|
9
|
+
router.get('/search', async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const q = req.query.q;
|
|
12
|
+
const anilistId = req.query.anilistId || null;
|
|
13
|
+
if (!q) return res.status(400).json({ error: 'query required' });
|
|
14
|
+
const results = await scraper.searchAll(q);
|
|
15
|
+
const data = results.map((r) => ({
|
|
16
|
+
session: r.session,
|
|
17
|
+
title: r.title,
|
|
18
|
+
year: r.year,
|
|
19
|
+
episodes: r.episodes,
|
|
20
|
+
poster: r.poster,
|
|
21
|
+
anilistId: r.anilistId,
|
|
22
|
+
}));
|
|
23
|
+
let autoMatch = null;
|
|
24
|
+
if (anilistId) {
|
|
25
|
+
autoMatch = data.find((r) => r.anilistId === String(anilistId)) || null;
|
|
26
|
+
}
|
|
27
|
+
res.json({ results: data, autoMatch });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
res.status(500).json({ error: e.message });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
router.post('/confirm', async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const { anilistTitle, session, title, anilistId } = req.body;
|
|
36
|
+
if (!anilistTitle || !session) {
|
|
37
|
+
return res.status(400).json({ error: 'anilistTitle and session required' });
|
|
38
|
+
}
|
|
39
|
+
await db.initDb();
|
|
40
|
+
let finalPoster = null;
|
|
41
|
+
const { getMediaById } = require('../anilist');
|
|
42
|
+
const id = anilistId || (db.getAnimeBySiteTitle(anilistTitle)?.anilist_id);
|
|
43
|
+
if (id) {
|
|
44
|
+
try {
|
|
45
|
+
const media = await getMediaById(id);
|
|
46
|
+
if (media?.coverImage) {
|
|
47
|
+
finalPoster = media.coverImage.extraLarge || media.coverImage.large || media.coverImage.medium;
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
db.saveConfirmedSiteMatch(anilistTitle, session, title || session, finalPoster);
|
|
52
|
+
res.json({ ok: true });
|
|
53
|
+
} catch (e) {
|
|
54
|
+
res.status(500).json({ error: e.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
module.exports = router;
|
package/src/api/runs.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Router } = require('express');
|
|
4
|
+
const db = require('../db');
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
router.get('/', async (req, res) => {
|
|
9
|
+
await db.initDb();
|
|
10
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
11
|
+
const runs = db.getRecentRuns(limit);
|
|
12
|
+
res.json(runs);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
module.exports = router;
|