ani-auto 1.4.4 → 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 +15 -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 +62 -11
- package/src/scraper.js +2 -2
- package/src/server.js +108 -0
- package/utils.js +21 -4
- 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>Download Center</h2>
|
|
5
|
+
<div class="subtitle">Select anime and start downloading</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<Card style="margin-bottom:16px">
|
|
9
|
+
<template #content>
|
|
10
|
+
<div class="form-row" style="margin-bottom:12px;max-width:400px">
|
|
11
|
+
<div class="form-group" style="margin-bottom:0">
|
|
12
|
+
<label>Quality</label>
|
|
13
|
+
<Select v-model="quality" :options="['1080p', '720p', '360p']" fluid />
|
|
14
|
+
</div>
|
|
15
|
+
<div class="form-group" style="margin-bottom:0">
|
|
16
|
+
<label>Language</label>
|
|
17
|
+
<Select v-model="language" :options="['sub', 'dub']" fluid />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
21
|
+
<Button label="Download All New Episodes" severity="primary" @click="startDownload" :disabled="downloadStore.running" />
|
|
22
|
+
<Button label="Retry Failed" @click="retryFailed" :disabled="downloadStore.running" />
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
</Card>
|
|
26
|
+
|
|
27
|
+
<template v-if="downloadStore.running || downloadStore.summary">
|
|
28
|
+
<Card v-if="downloadStore.summary" style="border-color:var(--status-done);margin-bottom:12px">
|
|
29
|
+
<template #content>
|
|
30
|
+
<div style="display:flex;gap:20px;align-items:center">
|
|
31
|
+
<div><Tag :value="downloadStore.summary.downloaded + ' downloaded'" severity="success" /></div>
|
|
32
|
+
<div><span style="color:var(--ink-500)">{{ downloadStore.summary.skipped }} skipped</span></div>
|
|
33
|
+
<div><Tag v-if="downloadStore.summary.failed > 0" :value="downloadStore.summary.failed + ' failed'" severity="danger" /></div>
|
|
34
|
+
<Button label="Clear" size="small" @click="downloadStore.reset()" style="margin-left:auto" />
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
</Card>
|
|
38
|
+
|
|
39
|
+
<Card v-for="(prog, id) in downloadStore.progress" :key="id" style="margin-bottom:10px">
|
|
40
|
+
<template #content>
|
|
41
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
|
42
|
+
<strong style="color:var(--text-bright);font-size:13px">{{ prog.title }}</strong>
|
|
43
|
+
<span style="font-size:11px;color:var(--ink-500)">{{ prog.done }} done / {{ prog.failed }} failed</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div v-for="(ep, num) in prog.episodes" :key="num" style="margin-bottom:4px">
|
|
46
|
+
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:1px">
|
|
47
|
+
<span>EP {{ num }}</span>
|
|
48
|
+
<span v-if="ep.done" style="color:var(--status-done)">Done</span>
|
|
49
|
+
<span v-else-if="ep.error" style="color:var(--status-fail)">Failed</span>
|
|
50
|
+
<span v-else>{{ ep.speed || '' }}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<ProgressBar v-if="ep.percent !== undefined && !ep.done" :value="ep.percent" style="height:4px" />
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
</Card>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<Card v-if="downloadStore.logs.length" style="margin-top:16px">
|
|
59
|
+
<template #title>
|
|
60
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
61
|
+
<span style="font-size:13px">Activity Log</span>
|
|
62
|
+
<Button label="Clear" size="small" @click="downloadStore.logs = []" />
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
<template #content>
|
|
66
|
+
<div ref="logContainer" style="max-height:300px;overflow-y:auto;font-family:var(--font-mono);font-size:11px;line-height:1.6;background:var(--bg-primary);padding:10px;border:1px solid var(--ink-100);border-radius:var(--radius)">
|
|
67
|
+
<div v-for="(l, i) in downloadStore.logs" :key="i" style="white-space:pre-wrap;word-break:break-all">
|
|
68
|
+
<span style="color:var(--ink-500)">{{ l.timestamp?.slice(11, 19) || '' }}</span>
|
|
69
|
+
<span :style="{ color: levelColor(l.level) }">[{{ l.level }}]</span>
|
|
70
|
+
{{ l.message }}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
</Card>
|
|
75
|
+
|
|
76
|
+
<template v-if="!downloadStore.running && !downloadStore.summary">
|
|
77
|
+
<h3 style="color:var(--text-bright);font-size:14px;margin-bottom:10px">Tracked Anime</h3>
|
|
78
|
+
<Card v-if="animeList.length === 0">
|
|
79
|
+
<template #content>
|
|
80
|
+
<div class="empty-content">
|
|
81
|
+
<h3>No anime tracked</h3>
|
|
82
|
+
<p>Add anime first via AniList Sync or manually.</p>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
</Card>
|
|
86
|
+
<div v-else class="list-group">
|
|
87
|
+
<div v-for="a in animeList" :key="a.id" class="anime-card-row">
|
|
88
|
+
<img v-if="a.poster_url" :src="a.poster_url" class="anime-poster" @error="handleImgError" />
|
|
89
|
+
<div class="anime-info">
|
|
90
|
+
<div class="anime-title">{{ a.anilist_title || a.title }}</div>
|
|
91
|
+
<div class="anime-meta">
|
|
92
|
+
{{ a.stats?.done || 0 }} done ·
|
|
93
|
+
{{ a.stats?.pending || 0 }} pending ·
|
|
94
|
+
{{ a.stats?.failed || 0 }} failed
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<Button v-if="a.confirmed_site_title" label="Download" severity="primary" size="small" @click="downloadAnime(a)" />
|
|
98
|
+
<Tag v-else value="Unmatched" severity="secondary" size="small" />
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<script setup>
|
|
106
|
+
import { ref, watch, nextTick, onMounted } from 'vue'
|
|
107
|
+
import { useDownloadStore } from '../stores/download'
|
|
108
|
+
import { api } from '../api/client'
|
|
109
|
+
|
|
110
|
+
const downloadStore = useDownloadStore()
|
|
111
|
+
const animeList = ref([])
|
|
112
|
+
const quality = ref('1080p')
|
|
113
|
+
const language = ref('sub')
|
|
114
|
+
const logContainer = ref(null)
|
|
115
|
+
|
|
116
|
+
const FALLBACK_POSTER = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='150' viewBox='0 0 100 150'%3E%3Crect fill='%23111111' width='100' height='150'/%3E%3Cpath fill='%23333333' d='M40 55l30 20-30 20z'/%3E%3C/svg%3E"
|
|
117
|
+
|
|
118
|
+
function handleImgError(e) {
|
|
119
|
+
e.target.src = FALLBACK_POSTER
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function levelColor(level) {
|
|
123
|
+
switch (level) {
|
|
124
|
+
case 'ok': return 'var(--status-done)'
|
|
125
|
+
case 'warn': return 'var(--status-pending)'
|
|
126
|
+
case 'err': return 'var(--status-fail)'
|
|
127
|
+
default: return 'var(--text-dim)'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
watch(() => downloadStore.logs.length, async () => {
|
|
132
|
+
await nextTick()
|
|
133
|
+
if (logContainer.value) logContainer.value.scrollTop = logContainer.value.scrollHeight
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
async function startDownload() {
|
|
137
|
+
await downloadStore.startDownload(quality.value, language.value)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function downloadAnime(a) {
|
|
141
|
+
await downloadStore.downloadAnime(a.id, quality.value, language.value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function retryFailed() {
|
|
145
|
+
await downloadStore.retryFailed()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
onMounted(async () => {
|
|
149
|
+
try {
|
|
150
|
+
animeList.value = await api.anime.list()
|
|
151
|
+
} catch (e) {}
|
|
152
|
+
})
|
|
153
|
+
</script>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>Match Anime</h2>
|
|
5
|
+
<div class="subtitle" v-if="anime">{{ anime.anilist_title || anime.title }}</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<Skeleton v-if="loading" width="100%" height="300px" />
|
|
9
|
+
|
|
10
|
+
<Card v-else-if="!anime">
|
|
11
|
+
<template #content>
|
|
12
|
+
<div class="empty-content"><h3>Anime not found</h3></div>
|
|
13
|
+
</template>
|
|
14
|
+
</Card>
|
|
15
|
+
|
|
16
|
+
<template v-else>
|
|
17
|
+
<Card style="margin-bottom:16px">
|
|
18
|
+
<template #content>
|
|
19
|
+
<div><strong style="color:var(--text-bright)">{{ anime.anilist_title || anime.title }}</strong></div>
|
|
20
|
+
<div style="color:var(--ink-500);font-size:12px;margin-top:4px">AniList ID: {{ anime.anilist_id || 'N/A' }}</div>
|
|
21
|
+
</template>
|
|
22
|
+
</Card>
|
|
23
|
+
|
|
24
|
+
<Card v-if="autoMatch" style="margin-bottom:16px;border-color:var(--status-done)">
|
|
25
|
+
<template #content>
|
|
26
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
27
|
+
<div>
|
|
28
|
+
<Tag value="Auto Match Found" severity="success" style="margin-bottom:4px" />
|
|
29
|
+
<div style="color:var(--text-bright);font-size:16px;margin-top:4px">{{ autoMatch.title }}</div>
|
|
30
|
+
<div style="color:var(--ink-500);font-size:12px">{{ autoMatch.year || '?' }} — {{ autoMatch.episodes || '?' }} eps</div>
|
|
31
|
+
</div>
|
|
32
|
+
<Button label="Confirm Match" severity="primary" @click="confirmMatch(autoMatch)" />
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
</Card>
|
|
36
|
+
|
|
37
|
+
<Card>
|
|
38
|
+
<template #title>Manual Search</template>
|
|
39
|
+
<template #content>
|
|
40
|
+
<div style="display:flex;gap:10px;margin-bottom:16px">
|
|
41
|
+
<InputText v-model="searchQuery" placeholder="Search by title..." fluid />
|
|
42
|
+
<Button label="Search API" @click="search" />
|
|
43
|
+
</div>
|
|
44
|
+
<div v-if="searchResults.length">
|
|
45
|
+
<div v-for="r in searchResults" :key="r.session" class="match-result-card" @click="confirmMatch(r)">
|
|
46
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
47
|
+
<div>
|
|
48
|
+
<div style="color:var(--text-bright)">{{ r.title }}</div>
|
|
49
|
+
<div style="color:var(--ink-500);font-size:12px">{{ r.year || '?' }} — {{ r.episodes || '?' }} eps</div>
|
|
50
|
+
</div>
|
|
51
|
+
<Button label="Select" severity="primary" size="small" @click.stop="confirmMatch(r)" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div v-else-if="searched" class="empty-content" style="padding:20px">
|
|
56
|
+
<h3>No results</h3>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
</Card>
|
|
60
|
+
</template>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup>
|
|
65
|
+
import { ref, onMounted } from 'vue'
|
|
66
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
67
|
+
import { api } from '../api/client'
|
|
68
|
+
|
|
69
|
+
const props = defineProps({ animeId: String })
|
|
70
|
+
const route = useRoute()
|
|
71
|
+
const router = useRouter()
|
|
72
|
+
|
|
73
|
+
const loading = ref(true)
|
|
74
|
+
const anime = ref(null)
|
|
75
|
+
const autoMatch = ref(null)
|
|
76
|
+
const searchResults = ref([])
|
|
77
|
+
const searchQuery = ref('')
|
|
78
|
+
const searched = ref(false)
|
|
79
|
+
|
|
80
|
+
async function load() {
|
|
81
|
+
const id = props.animeId || route.params.animeId
|
|
82
|
+
try {
|
|
83
|
+
anime.value = await api.anime.get(id)
|
|
84
|
+
searchQuery.value = anime.value.anilist_title || anime.value.title
|
|
85
|
+
await search()
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error(e)
|
|
88
|
+
} finally {
|
|
89
|
+
loading.value = false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function search() {
|
|
94
|
+
searched.value = true
|
|
95
|
+
try {
|
|
96
|
+
const data = await api.match.search(searchQuery.value, anime.value?.anilist_id)
|
|
97
|
+
searchResults.value = data.results || []
|
|
98
|
+
autoMatch.value = data.autoMatch || null
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(e)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function confirmMatch(match) {
|
|
105
|
+
const title = anime.value.anilist_title || anime.value.title
|
|
106
|
+
await api.match.confirm({
|
|
107
|
+
anilistTitle: title,
|
|
108
|
+
session: match.session,
|
|
109
|
+
title: match.title,
|
|
110
|
+
anilistId: anime.value.anilist_id,
|
|
111
|
+
})
|
|
112
|
+
router.push(`/anime/${anime.value.id}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onMounted(load)
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<style scoped>
|
|
119
|
+
.match-result-card {
|
|
120
|
+
padding: 12px;
|
|
121
|
+
border: 1px solid var(--ink-100);
|
|
122
|
+
border-radius: var(--radius);
|
|
123
|
+
margin-bottom: 8px;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition: border-color 0.15s, background 0.15s;
|
|
126
|
+
}
|
|
127
|
+
.match-result-card:hover {
|
|
128
|
+
border-color: var(--ink-300);
|
|
129
|
+
background: var(--bg-hover);
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="onboarding">
|
|
3
|
+
<div class="onboarding-box">
|
|
4
|
+
<div class="logo">ANI-AUTO</div>
|
|
5
|
+
<p v-if="status === 'processing'">Verifying token...</p>
|
|
6
|
+
<p v-if="status === 'done'" style="color:var(--status-done)">Connected! Closing window...</p>
|
|
7
|
+
<p v-if="status === 'error'" style="color:var(--status-fail)">Failed: {{ error }}</p>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
import { ref, onMounted } from 'vue'
|
|
14
|
+
import { api } from '../api/client'
|
|
15
|
+
|
|
16
|
+
const status = ref('processing')
|
|
17
|
+
const error = ref('')
|
|
18
|
+
|
|
19
|
+
onMounted(async () => {
|
|
20
|
+
const hash = window.location.hash.substring(1)
|
|
21
|
+
const params = new URLSearchParams(hash)
|
|
22
|
+
const token = params.get('access_token')
|
|
23
|
+
if (!token) {
|
|
24
|
+
status.value = 'error'
|
|
25
|
+
error.value = 'No token found in URL'
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const res = await api.config.verifyToken(token)
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
await api.config.update({ anilistToken: token, anilistUsername: res.name, anilistAvatar: res.avatar })
|
|
32
|
+
status.value = 'done'
|
|
33
|
+
window.opener?.postMessage({ type: 'anilist-token', token }, '*')
|
|
34
|
+
setTimeout(() => window.close(), 1000)
|
|
35
|
+
} else {
|
|
36
|
+
status.value = 'error'
|
|
37
|
+
error.value = res.error
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
status.value = 'error'
|
|
41
|
+
error.value = e.message
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="onboarding">
|
|
3
|
+
<div class="onboarding-box">
|
|
4
|
+
<div class="logo">ANI-AUTO</div>
|
|
5
|
+
<div class="logo-sub">Automatic Anime Downloader</div>
|
|
6
|
+
|
|
7
|
+
<div v-if="step === 'welcome'">
|
|
8
|
+
<h2>Welcome</h2>
|
|
9
|
+
<p>Connect your AniList account to sync your watch list and automatically download new episodes. Or skip and use manual mode.</p>
|
|
10
|
+
<div class="btn-group">
|
|
11
|
+
<Button label="Connect AniList" severity="primary" @click="connectAnilist" />
|
|
12
|
+
<Button label="Skip — Manual Mode" @click="skipAnilist" />
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div v-if="step === 'cf-bypass'">
|
|
17
|
+
<h2>Cloudflare Bypass</h2>
|
|
18
|
+
<p>AnimePahe requires a cf_clearance cookie to bypass Cloudflare. We'll help you get it.</p>
|
|
19
|
+
<div class="btn-group" style="margin-top:20px">
|
|
20
|
+
<Button label="Auto-Detect Cookies" severity="primary" @click="startCfBypass" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div v-if="step === 'telegram'">
|
|
25
|
+
<h2>Notifications</h2>
|
|
26
|
+
<p>Optional: Get Telegram notifications when episodes download.</p>
|
|
27
|
+
<div class="form-group" style="text-align:left">
|
|
28
|
+
<label>Telegram Chat ID</label>
|
|
29
|
+
<InputText v-model="telegramChatId" placeholder="e.g. 123456789" fluid />
|
|
30
|
+
<p style="color:var(--ink-500);font-size:11px;margin-top:4px">Message @userinfobot on Telegram to get your ID</p>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="btn-group" style="margin-top:20px">
|
|
33
|
+
<Button label="Save & Continue" severity="primary" @click="saveTelegram" />
|
|
34
|
+
<Button label="Skip Telegram" @click="finish" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup>
|
|
42
|
+
import { ref } from 'vue'
|
|
43
|
+
import { useRouter } from 'vue-router'
|
|
44
|
+
import { api } from '../api/client'
|
|
45
|
+
import { useConfigStore } from '../stores/config'
|
|
46
|
+
|
|
47
|
+
const router = useRouter()
|
|
48
|
+
const configStore = useConfigStore()
|
|
49
|
+
const step = ref('welcome')
|
|
50
|
+
const telegramChatId = ref('')
|
|
51
|
+
const tokenCaptured = ref(false)
|
|
52
|
+
async function connectAnilist() {
|
|
53
|
+
const { url } = await api.config.oauthUrl()
|
|
54
|
+
const w = window.open(url, 'anilist-oauth', 'width=600,height=700')
|
|
55
|
+
const timer = setInterval(() => {
|
|
56
|
+
if (w.closed) {
|
|
57
|
+
clearInterval(timer)
|
|
58
|
+
if (!tokenCaptured.value) {
|
|
59
|
+
step.value = 'cf-bypass'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, 500)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function skipAnilist() {
|
|
66
|
+
step.value = 'cf-bypass'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function startCfBypass() {
|
|
70
|
+
router.push({ name: 'cf-result' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function saveTelegram() {
|
|
74
|
+
if (telegramChatId.value) {
|
|
75
|
+
await api.config.update({ telegramChatId: telegramChatId.value })
|
|
76
|
+
}
|
|
77
|
+
await finish()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function finish() {
|
|
81
|
+
await api.config.update({ onboardingSkipped: true })
|
|
82
|
+
await configStore.fetch()
|
|
83
|
+
router.push('/')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
window.addEventListener('message', async (e) => {
|
|
87
|
+
if (e.data?.type === 'anilist-token') {
|
|
88
|
+
tokenCaptured.value = true
|
|
89
|
+
await configStore.fetch()
|
|
90
|
+
step.value = 'cf-bypass'
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>Run History</h2>
|
|
5
|
+
<div class="subtitle">{{ runs.length }} runs recorded</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<Skeleton v-if="loading" width="100%" height="300px" />
|
|
9
|
+
|
|
10
|
+
<template v-else-if="runs.length === 0">
|
|
11
|
+
<Card>
|
|
12
|
+
<template #content>
|
|
13
|
+
<div class="empty-content">
|
|
14
|
+
<h3>No runs yet</h3>
|
|
15
|
+
<p>Run a download cycle to see history here.</p>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
</Card>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<template v-else>
|
|
22
|
+
<div class="stat-row">
|
|
23
|
+
<Card>
|
|
24
|
+
<template #content>
|
|
25
|
+
<div class="stat-inner">
|
|
26
|
+
<div class="stat-value" style="color:var(--status-done)">{{ totals.downloaded }}</div>
|
|
27
|
+
<div class="stat-label">Total Downloaded</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
</Card>
|
|
31
|
+
<Card>
|
|
32
|
+
<template #content>
|
|
33
|
+
<div class="stat-inner">
|
|
34
|
+
<div class="stat-value" style="color:var(--status-fail)">{{ totals.failed }}</div>
|
|
35
|
+
<div class="stat-label">Total Failed</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
</Card>
|
|
39
|
+
<Card>
|
|
40
|
+
<template #content>
|
|
41
|
+
<div class="stat-inner">
|
|
42
|
+
<div class="stat-value" style="color:var(--ink-500)">{{ totals.skipped }}</div>
|
|
43
|
+
<div class="stat-label">Total Skipped</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
</Card>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<DataTable :value="runs" paginator :rows="20" :rowsPerPageOptions="[10,20,50]"
|
|
50
|
+
dataKey="id" :sortField="sortField" :sortOrder="sortOrder" @sort="onSort"
|
|
51
|
+
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink"
|
|
52
|
+
currentPageReportTemplate="{currentPage} of {totalPages}"
|
|
53
|
+
stripedRows>
|
|
54
|
+
<Column field="id" header="#" sortable style="width:60px">
|
|
55
|
+
<template #body="{ data }">
|
|
56
|
+
<span style="font-family:var(--font-mono);color:var(--ink-500)">{{ data.id }}</span>
|
|
57
|
+
</template>
|
|
58
|
+
</Column>
|
|
59
|
+
<Column field="ran_at" header="Time" sortable>
|
|
60
|
+
<template #body="{ data }">
|
|
61
|
+
<span style="font-family:var(--font-mono);font-size:12px">{{ data.ran_at }}</span>
|
|
62
|
+
</template>
|
|
63
|
+
</Column>
|
|
64
|
+
<Column field="downloaded" header="Downloaded" sortable>
|
|
65
|
+
<template #body="{ data }">
|
|
66
|
+
<Tag :value="data.downloaded" severity="success" v-if="data.downloaded > 0" />
|
|
67
|
+
<span v-else style="color:var(--ink-500)">{{ data.downloaded }}</span>
|
|
68
|
+
</template>
|
|
69
|
+
</Column>
|
|
70
|
+
<Column field="skipped" header="Skipped" sortable>
|
|
71
|
+
<template #body="{ data }">
|
|
72
|
+
<span style="color:var(--ink-500)">{{ data.skipped }}</span>
|
|
73
|
+
</template>
|
|
74
|
+
</Column>
|
|
75
|
+
<Column field="failed" header="Failed" sortable>
|
|
76
|
+
<template #body="{ data }">
|
|
77
|
+
<Tag :value="data.failed" severity="danger" v-if="data.failed > 0" />
|
|
78
|
+
<span v-else style="color:var(--ink-500)">{{ data.failed }}</span>
|
|
79
|
+
</template>
|
|
80
|
+
</Column>
|
|
81
|
+
<Column field="notes" header="Notes">
|
|
82
|
+
<template #body="{ data }">
|
|
83
|
+
<span style="font-size:12px;color:var(--ink-700)">{{ data.notes || '-' }}</span>
|
|
84
|
+
</template>
|
|
85
|
+
</Column>
|
|
86
|
+
</DataTable>
|
|
87
|
+
</template>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup>
|
|
92
|
+
import { ref, computed, onMounted } from 'vue'
|
|
93
|
+
import { api } from '../api/client'
|
|
94
|
+
import DataTable from 'primevue/datatable'
|
|
95
|
+
import Column from 'primevue/column'
|
|
96
|
+
|
|
97
|
+
const loading = ref(true)
|
|
98
|
+
const runs = ref([])
|
|
99
|
+
const sortField = ref('ran_at')
|
|
100
|
+
const sortOrder = ref(-1)
|
|
101
|
+
|
|
102
|
+
const totals = computed(() => ({
|
|
103
|
+
downloaded: runs.value.reduce((s, r) => s + r.downloaded, 0),
|
|
104
|
+
skipped: runs.value.reduce((s, r) => s + r.skipped, 0),
|
|
105
|
+
failed: runs.value.reduce((s, r) => s + r.failed, 0),
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
function onSort(event) {
|
|
109
|
+
sortField.value = event.sortField
|
|
110
|
+
sortOrder.value = event.sortOrder
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onMounted(async () => {
|
|
114
|
+
try {
|
|
115
|
+
runs.value = await api.runs.list(50)
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error(e)
|
|
118
|
+
} finally {
|
|
119
|
+
loading.value = false
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
</script>
|