ani-auto 1.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/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # ani-auto
2
+
3
+ Automatic anime episode downloader.
4
+ Watches your **AniList** list (or a manual list) and downloads new episodes the moment they appear on the site — with full quality/language control, a background daemon, cron scheduling etc.
5
+ ---
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 18+
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # ani-auto/ ← this project
17
+
18
+ cd ani-auto
19
+ npm install
20
+
21
+ # 2. Link the CLI globally (optional)
22
+ npm link
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ # First-time setup wizard
31
+ ani-auto setup
32
+
33
+ # Manually trigger one download cycle
34
+ ani-auto check
35
+
36
+ # Start the background daemon
37
+ ani-auto daemon
38
+ # or: npm start
39
+ ```
40
+
41
+ ---
42
+
43
+ ## How It Works
44
+
45
+ 1. **AniList** — queries your list for the configured statuses (e.g. `CURRENT`).
46
+ For each anime, it searches the download site, fetches the episode list, and downloads anything not already in the database.
47
+
48
+ 2. **Manual list** — entries added via `ani-auto add` are checked the same way.
49
+ Manual entries support per-anime quality/language overrides.
50
+
51
+ 3. **State** — SQLite (`~/.config/ani-auto/state.db`) tracks every episode as `pending → downloading → done / failed`. Already-downloaded episodes are never re-fetched.
52
+
53
+ 4. **Scheduling** — the daemon runs on both:
54
+ - A **polling interval** (default: every 30 min)
55
+ - A **cron schedule** (default: `0 */2 * * *` — every 2 hours)
56
+
57
+ ---
58
+
59
+ ## Commands
60
+
61
+ | Command | Description |
62
+ |---|---|
63
+ | `setup` | Interactive first-time wizard |
64
+ | `check` | Run one download cycle now |
65
+ | `list` | Show all tracked anime |
66
+ | `add "<title>"` | Add to manual list |
67
+ | `remove "<title>"` | Remove from tracking |
68
+ | `enable "<title>"` | Re-enable downloads |
69
+ | `disable "<title>"` | Pause without removing |
70
+ | `status` | View recent run log |
71
+ | `config` | View / update settings |
72
+ | `daemon` | Start background daemon |
73
+
74
+ ---
75
+
76
+ ## Manual Add Options
77
+
78
+ ```bash
79
+ # Default quality/language from global config
80
+ ani-auto add "Frieren: Beyond Journey's End"
81
+
82
+ # Per-anime overrides
83
+ ani-auto add "One Piece" --quality 720p --language dub
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Config Options
89
+
90
+ ```bash
91
+ ani-auto config --set-quality 1080p
92
+ ani-auto config --set-language sub
93
+ ani-auto config --set-concurrency 3
94
+ ani-auto config --set-output ~/Videos/Anime
95
+ ani-auto config --set-interval 60 # daemon: poll every 60 min
96
+ ani-auto config --set-cron "0 6 * * *" # also trigger at 6 AM daily
97
+ ```
98
+
99
+ Full config lives at `~/.config/ani-auto/config.json`.
100
+
101
+ ---
102
+
103
+ ## AniList Statuses
104
+
105
+ Configured during `setup` or by editing `config.json`:
106
+
107
+ | Status | Meaning |
108
+ |---|---|
109
+ | `CURRENT` | Currently watching (recommended) |
110
+ | `PLANNING` | Plan to watch |
111
+ | `PAUSED` | Paused |
112
+ | `REPEATING` | Re-watching |
113
+
114
+ ---
115
+
116
+
117
+
118
+ ---
119
+
120
+ ## File Structure
121
+
122
+ ```
123
+ ani-auto/
124
+ ├── src/
125
+ │ ├── cli.js ← entry point for all commands
126
+ │ ├── daemon.js ← background daemon
127
+ │ ├── engine.js ← core run cycle logic
128
+ │ ├── scraper.js ← adapter over k-anime utils
129
+ │ ├── anilist.js ← AniList GraphQL client
130
+ │ ├── db.js ← SQLite state layer
131
+ │ └── config.js ← JSON config manager
132
+ ├── package.json
133
+ ├── utils.js
134
+ └── README.md
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Systemd Service (Linux/Arch)
140
+
141
+ ```ini
142
+ # ~/.config/systemd/user/ani-auto.service
143
+ [Unit]
144
+ Description=K-Anime Auto Downloader
145
+
146
+ [Service]
147
+ ExecStart=/usr/bin/node /path/to/ani-auto/src/daemon.js
148
+ Restart=on-failure
149
+ RestartSec=10
150
+
151
+ [Install]
152
+ WantedBy=default.target
153
+ ```
154
+
155
+ ```bash
156
+ systemctl --user enable --now ani-auto
157
+ systemctl --user status ani-auto
158
+ journalctl --user -u ani-auto -f
159
+ ```
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ani-auto",
3
+ "version": "1.0.0",
4
+ "description": "Automatic anime episode downloader",
5
+ "bin": {
6
+ "ani-auto": "./src/cli.js"
7
+ },
8
+ "scripts": {
9
+ "start": "node src/daemon.js",
10
+ "check": "node src/cli.js check",
11
+ "setup": "node src/cli.js setup"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^4.1.2",
15
+ "cli-progress": "^3.12.0",
16
+ "commander": "^12.0.0",
17
+ "cron": "^3.1.6",
18
+ "inquirer": "^8.2.6",
19
+ "node-fetch": "^2.7.0",
20
+ "ora": "^5.4.1",
21
+ "sql.js": "^1.12.0"
22
+ }
23
+ }
package/src/anilist.js ADDED
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const fetch = require('node-fetch');
4
+
5
+ const ANILIST_GQL = 'https://graphql.anilist.co';
6
+
7
+ // ── Queries ────────────────────────────────────────────────────────────────
8
+
9
+ const MEDIA_LIST_QUERY = `
10
+ query ($userName: String, $status: MediaListStatus) {
11
+ MediaListCollection(userName: $userName, type: ANIME, status: $status) {
12
+ lists {
13
+ entries {
14
+ mediaId
15
+ status
16
+ progress
17
+ media {
18
+ id
19
+ title { romaji english native }
20
+ episodes
21
+ status
22
+ nextAiringEpisode { episode airingAt }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }`;
28
+
29
+ const VIEWER_QUERY = `
30
+ query {
31
+ Viewer { id name }
32
+ }`;
33
+
34
+ const SEARCH_QUERY = `
35
+ query ($search: String) {
36
+ Page(page: 1, perPage: 8) {
37
+ media(search: $search, type: ANIME) {
38
+ id
39
+ title { romaji english native }
40
+ episodes
41
+ status
42
+ seasonYear
43
+ format
44
+ }
45
+ }
46
+ }`;
47
+
48
+ // ── Helpers ────────────────────────────────────────────────────────────────
49
+
50
+ async function gqlRequest(query, variables = {}, token = null) {
51
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
52
+ if (token) headers['Authorization'] = `Bearer ${token}`;
53
+
54
+ const res = await fetch(ANILIST_GQL, {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify({ query, variables }),
58
+ });
59
+
60
+ if (!res.ok) {
61
+ const text = await res.text();
62
+ throw new Error(`AniList API error ${res.status}: ${text}`);
63
+ }
64
+
65
+ const json = await res.json();
66
+ if (json.errors) throw new Error(`AniList GQL error: ${json.errors[0].message}`);
67
+ return json.data;
68
+ }
69
+
70
+ // ── Public API ─────────────────────────────────────────────────────────────
71
+
72
+ async function getViewer(token) {
73
+ const data = await gqlRequest(VIEWER_QUERY, {}, token);
74
+ return data.Viewer;
75
+ }
76
+
77
+ async function getUserList(userName, statuses, token = null) {
78
+ const results = [];
79
+ for (const status of statuses) {
80
+ const data = await gqlRequest(MEDIA_LIST_QUERY, { userName, status }, token);
81
+ const lists = data?.MediaListCollection?.lists || [];
82
+ for (const list of lists) {
83
+ for (const entry of list.entries) {
84
+ results.push({
85
+ anilistId: entry.mediaId,
86
+ status: entry.status,
87
+ progress: entry.progress,
88
+ totalEpisodes: entry.media.episodes,
89
+ animeStatus: entry.media.status,
90
+ titles: {
91
+ romaji: entry.media.title.romaji,
92
+ english: entry.media.title.english,
93
+ native: entry.media.title.native,
94
+ },
95
+ nextAiring: entry.media.nextAiringEpisode || null,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ return results;
101
+ }
102
+
103
+ /**
104
+ * Search AniList for anime by title.
105
+ * Returns up to 8 results with id, titles, episodes, status, year.
106
+ */
107
+ async function searchAnime(query, token = null) {
108
+ const data = await gqlRequest(SEARCH_QUERY, { search: query }, token);
109
+ return (data?.Page?.media || []).map(m => ({
110
+ anilistId: m.id,
111
+ titles: {
112
+ romaji: m.title.romaji,
113
+ english: m.title.english,
114
+ native: m.title.native,
115
+ },
116
+ episodes: m.episodes,
117
+ status: m.status,
118
+ year: m.seasonYear,
119
+ format: m.format,
120
+ }));
121
+ }
122
+
123
+ function pickTitle(titles) {
124
+ return titles.english || titles.romaji || titles.native;
125
+ }
126
+
127
+ module.exports = {
128
+ getViewer,
129
+ getUserList,
130
+ searchAnime,
131
+ pickTitle,
132
+ };