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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { api } from '../api/client'
|
|
3
|
+
|
|
4
|
+
export const useUpdateStore = defineStore('update', {
|
|
5
|
+
state: () => ({
|
|
6
|
+
checking: false,
|
|
7
|
+
currentVersion: '',
|
|
8
|
+
latestVersion: '',
|
|
9
|
+
updateAvailable: false,
|
|
10
|
+
checkError: null,
|
|
11
|
+
installing: false,
|
|
12
|
+
installProgress: 0,
|
|
13
|
+
installMessage: '',
|
|
14
|
+
installDone: false,
|
|
15
|
+
installError: null,
|
|
16
|
+
}),
|
|
17
|
+
actions: {
|
|
18
|
+
async check() {
|
|
19
|
+
this.checking = true
|
|
20
|
+
this.checkError = null
|
|
21
|
+
try {
|
|
22
|
+
const res = await api.update.check()
|
|
23
|
+
this.currentVersion = res.current
|
|
24
|
+
this.latestVersion = res.latest
|
|
25
|
+
this.updateAvailable = res.updateAvailable
|
|
26
|
+
} catch (e) {
|
|
27
|
+
this.checkError = e.message
|
|
28
|
+
} finally {
|
|
29
|
+
this.checking = false
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async install() {
|
|
33
|
+
this.installing = true
|
|
34
|
+
this.installProgress = 0
|
|
35
|
+
this.installMessage = 'Starting...'
|
|
36
|
+
this.installDone = false
|
|
37
|
+
this.installError = null
|
|
38
|
+
try {
|
|
39
|
+
await api.update.install()
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.installError = e.message
|
|
42
|
+
this.installing = false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
handleEvent(msg) {
|
|
46
|
+
switch (msg.type) {
|
|
47
|
+
case 'update:progress':
|
|
48
|
+
this.installProgress = msg.percent
|
|
49
|
+
this.installMessage = msg.message
|
|
50
|
+
break
|
|
51
|
+
case 'update:done':
|
|
52
|
+
this.installProgress = 100
|
|
53
|
+
this.installMessage = msg.message
|
|
54
|
+
this.installDone = true
|
|
55
|
+
this.installing = false
|
|
56
|
+
this.updateAvailable = false
|
|
57
|
+
this.latestVersion = msg.version
|
|
58
|
+
break
|
|
59
|
+
case 'update:error':
|
|
60
|
+
this.installError = msg.error
|
|
61
|
+
this.installing = false
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
reset() {
|
|
66
|
+
this.installing = false
|
|
67
|
+
this.installProgress = 0
|
|
68
|
+
this.installMessage = ''
|
|
69
|
+
this.installDone = false
|
|
70
|
+
this.installError = null
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>About</h2>
|
|
5
|
+
</div>
|
|
6
|
+
<Card>
|
|
7
|
+
<template #content>
|
|
8
|
+
<div style="text-align:center;padding:40px">
|
|
9
|
+
<pre class="about-banner">{{ banner }}</pre>
|
|
10
|
+
<div style="font-size:12px;color:var(--ink-500);letter-spacing:3px;text-transform:uppercase;margin-bottom:20px">
|
|
11
|
+
Automatic Anime Downloader — v1.4.4
|
|
12
|
+
</div>
|
|
13
|
+
<div style="color:var(--text-dim);font-size:13px;max-width:400px;margin:0 auto;line-height:1.8">
|
|
14
|
+
Automatically downloads new anime episodes.<br />
|
|
15
|
+
Works with your AniList watching list or manual mode.
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
</Card>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
const banner = `
|
|
25
|
+
█████╗ ███╗ ██╗██╗ █████╗ ██╗ ██╗████████╗ ██████╗
|
|
26
|
+
██╔══██╗████╗ ██║██║ ██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗
|
|
27
|
+
███████║██╔██╗ ██║██║ ███████║██║ ██║ ██║ ██║ ██║
|
|
28
|
+
██╔══██║██║╚██╗██║██║ ██╔══██║██║ ██║ ██║ ██║ ██║
|
|
29
|
+
██║ ██║██║ ╚████║██║ ██║ ██║╚██████╔╝ ██║ ╚██████╔╝
|
|
30
|
+
╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝
|
|
31
|
+
`
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.about-banner {
|
|
36
|
+
font-family: var(--font-logo);
|
|
37
|
+
font-size: 10px;
|
|
38
|
+
line-height: 1.2;
|
|
39
|
+
color: var(--red-primary);
|
|
40
|
+
margin-bottom: 16px;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>AniList Sync</h2>
|
|
5
|
+
<div class="subtitle">Browse your AniList and add to tracking</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="anilist-toolbar">
|
|
9
|
+
<Select v-model="selectedStatus" @change="fetchList" :options="statusOptions" optionValue="value" style="width:180px">
|
|
10
|
+
<template #value="{ value }">
|
|
11
|
+
{{ statusOptions.find(o => o.value === value)?.label || value }}
|
|
12
|
+
</template>
|
|
13
|
+
<template #option="{ option }">
|
|
14
|
+
{{ option.label }}
|
|
15
|
+
</template>
|
|
16
|
+
</Select>
|
|
17
|
+
<Button label="Refresh" @click="fetchList" />
|
|
18
|
+
<div style="flex:1;min-width:10px"></div>
|
|
19
|
+
<InputText v-model="manualTitle" placeholder="Search AniList..." style="width:220px" />
|
|
20
|
+
<Button label="Search" @click="searchAnilist" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<Skeleton v-if="loading" width="100%" height="300px" />
|
|
24
|
+
|
|
25
|
+
<div v-else-if="searchResults.length">
|
|
26
|
+
<h3 style="color:var(--text-bright);font-size:14px;margin-bottom:12px">Search Results</h3>
|
|
27
|
+
<div class="list-group">
|
|
28
|
+
<div v-for="r in searchResults" :key="r.anilistId" class="anime-card-row" @click="addFromSearch(r)">
|
|
29
|
+
<img v-if="r.posterUrl" :src="r.posterUrl" class="anime-poster" @error="handleImgError" />
|
|
30
|
+
<div class="anime-info">
|
|
31
|
+
<div class="anime-title">{{ r.titles?.english || r.titles?.romaji }}</div>
|
|
32
|
+
<div class="anime-meta">{{ r.year || '?' }} — {{ r.episodes || '?' }} eps — {{ r.status }}</div>
|
|
33
|
+
</div>
|
|
34
|
+
<Button label="Add" severity="primary" size="small" @click.stop="addFromSearch(r)" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div v-else-if="anilistEntries.length">
|
|
40
|
+
<h3 style="color:var(--text-bright);font-size:14px;margin-bottom:12px">{{ selectedStatus }} ({{ anilistEntries.length }})</h3>
|
|
41
|
+
<div class="list-group">
|
|
42
|
+
<div v-for="entry in anilistEntries" :key="entry.anilistId" class="anime-card-row" @click="addEntry(entry)">
|
|
43
|
+
<img v-if="entry.posterUrl" :src="entry.posterUrl" class="anime-poster" @error="handleImgError" />
|
|
44
|
+
<div class="anime-info">
|
|
45
|
+
<div class="anime-title">{{ entry.titles?.english || entry.titles?.romaji }}</div>
|
|
46
|
+
<div class="anime-meta">{{ entry.progress || 0 }}/{{ entry.totalEpisodes || '?' }} eps — {{ entry.animeStatus }}</div>
|
|
47
|
+
</div>
|
|
48
|
+
<Button label="Track" severity="primary" size="small" @click.stop="addEntry(entry)" />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<Card v-else>
|
|
54
|
+
<template #content>
|
|
55
|
+
<div class="empty-content">
|
|
56
|
+
<h3>No entries found</h3>
|
|
57
|
+
<p>Connect AniList in Settings or add manually.</p>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
</Card>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup>
|
|
65
|
+
import { ref, onMounted } from 'vue'
|
|
66
|
+
import { api } from '../api/client'
|
|
67
|
+
|
|
68
|
+
const loading = ref(false)
|
|
69
|
+
const selectedStatus = ref('CURRENT')
|
|
70
|
+
const manualTitle = ref('')
|
|
71
|
+
const anilistEntries = ref([])
|
|
72
|
+
const searchResults = ref([])
|
|
73
|
+
|
|
74
|
+
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"
|
|
75
|
+
|
|
76
|
+
function handleImgError(e) {
|
|
77
|
+
e.target.src = FALLBACK_POSTER
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const statusOptions = [
|
|
81
|
+
{ label: 'CURRENT (Watching)', value: 'CURRENT' },
|
|
82
|
+
{ label: 'PLANNING', value: 'PLANNING' },
|
|
83
|
+
{ label: 'PAUSED', value: 'PAUSED' },
|
|
84
|
+
{ label: 'REPEATING', value: 'REPEATING' },
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
async function fetchList() {
|
|
88
|
+
loading.value = true
|
|
89
|
+
try {
|
|
90
|
+
anilistEntries.value = await api.anilist.list(selectedStatus.value)
|
|
91
|
+
searchResults.value = []
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(e)
|
|
94
|
+
} finally {
|
|
95
|
+
loading.value = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function searchAnilist() {
|
|
100
|
+
if (!manualTitle.value) return
|
|
101
|
+
loading.value = true
|
|
102
|
+
try {
|
|
103
|
+
searchResults.value = await api.anilist.search(manualTitle.value)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(e)
|
|
106
|
+
} finally {
|
|
107
|
+
loading.value = false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function addEntry(entry) {
|
|
112
|
+
const title = entry.titles?.english || entry.titles?.romaji
|
|
113
|
+
await api.anime.add({ title, anilistId: entry.anilistId, poster_url: entry.posterUrl })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function addFromSearch(r) {
|
|
117
|
+
const title = r.titles?.english || r.titles?.romaji
|
|
118
|
+
await api.anime.add({ title, anilistId: r.anilistId, poster_url: r.posterUrl })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onMounted(fetchList)
|
|
122
|
+
</script>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>{{ anime?.anilist_title || anime?.title || 'Anime' }}</h2>
|
|
5
|
+
<div class="subtitle">
|
|
6
|
+
<router-link to="/anime">← Back to Anime</router-link>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<Skeleton v-if="loading" width="100%" height="400px" />
|
|
11
|
+
|
|
12
|
+
<Card v-else-if="!anime">
|
|
13
|
+
<template #content>
|
|
14
|
+
<div class="empty-content"><h3>Anime not found</h3></div>
|
|
15
|
+
</template>
|
|
16
|
+
</Card>
|
|
17
|
+
|
|
18
|
+
<template v-else>
|
|
19
|
+
<Card style="margin-bottom:16px">
|
|
20
|
+
<template #content>
|
|
21
|
+
<div class="anime-detail-layout">
|
|
22
|
+
<img v-if="anime.poster_url" :src="anime.poster_url" class="anime-detail-poster" @error="handleImgError" />
|
|
23
|
+
<div style="flex:1">
|
|
24
|
+
<div style="margin-bottom:12px">
|
|
25
|
+
<Tag :value="anime.confirmed_site_title ? 'Matched' : 'Not Matched'" :severity="anime.confirmed_site_title ? 'success' : 'secondary'" />
|
|
26
|
+
<Tag :value="anime.enabled ? 'Enabled' : 'Disabled'" :severity="anime.enabled ? 'success' : 'danger'" style="margin-left:6px" />
|
|
27
|
+
<Tag :value="anime.source" severity="info" style="margin-left:6px" />
|
|
28
|
+
</div>
|
|
29
|
+
<div v-if="anime.confirmed_site_title" style="font-size:12px;color:var(--ink-500);margin-bottom:12px">
|
|
30
|
+
Matched to: {{ anime.confirmed_site_title }}
|
|
31
|
+
<a @click="$router.push(`/match/${anime.id}`)" style="margin-left:8px;cursor:pointer">change</a>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row" style="margin-bottom:12px;max-width:400px">
|
|
34
|
+
<div class="form-group" style="margin-bottom:0">
|
|
35
|
+
<label>Quality</label>
|
|
36
|
+
<Select v-model="overrideQuality" @change="setQuality" :options="qualityOptions" optionLabel="label" optionValue="value" fluid />
|
|
37
|
+
</div>
|
|
38
|
+
<div class="form-group" style="margin-bottom:0">
|
|
39
|
+
<label>Language</label>
|
|
40
|
+
<Select v-model="overrideLanguage" @change="setLanguage" :options="['sub', 'dub']" fluid />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
44
|
+
<Button label="Download Unwatched" severity="primary" @click="downloadAnime" />
|
|
45
|
+
<Button v-if="!anime.confirmed_site_title" label="Find Match" @click="$router.push(`/match/${anime.id}`)" />
|
|
46
|
+
<Button :label="anime.enabled ? 'Disable' : 'Enable'" @click="toggleEnabled" />
|
|
47
|
+
<Button label="Remove" severity="danger" @click="removeAnime" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
</Card>
|
|
53
|
+
|
|
54
|
+
<Tabs v-model:value="epFilter">
|
|
55
|
+
<TabList>
|
|
56
|
+
<Tab value="">All ({{ episodes.length }})</Tab>
|
|
57
|
+
<Tab value="done">Done ({{ stats.done }})</Tab>
|
|
58
|
+
<Tab value="failed">Failed ({{ stats.failed }})</Tab>
|
|
59
|
+
<Tab value="pending">Pending ({{ stats.pending }})</Tab>
|
|
60
|
+
</TabList>
|
|
61
|
+
<TabPanels>
|
|
62
|
+
<TabPanel :value="epFilter">
|
|
63
|
+
<DataTable v-if="filteredEpisodes.length" :value="filteredEpisodes" dataKey="id" stripedRows>
|
|
64
|
+
<Column field="episode_num" header="EP">
|
|
65
|
+
<template #body="{ data }">
|
|
66
|
+
<span style="font-family:var(--font-mono);font-size:12px">{{ data.episode_num }}</span>
|
|
67
|
+
</template>
|
|
68
|
+
</Column>
|
|
69
|
+
<Column field="status" header="Status">
|
|
70
|
+
<template #body="{ data }">
|
|
71
|
+
<Tag :value="data.status" :severity="statusSeverity(data.status)" size="small" />
|
|
72
|
+
</template>
|
|
73
|
+
</Column>
|
|
74
|
+
<Column field="filename" header="Filename">
|
|
75
|
+
<template #body="{ data }">
|
|
76
|
+
<span style="font-size:12px;color:var(--ink-700)">{{ data.filename || '-' }}</span>
|
|
77
|
+
</template>
|
|
78
|
+
</Column>
|
|
79
|
+
<Column field="downloaded_at" header="Downloaded">
|
|
80
|
+
<template #body="{ data }">
|
|
81
|
+
<span style="font-size:12px;color:var(--ink-700)">{{ data.downloaded_at ? data.downloaded_at.slice(0, 10) : '-' }}</span>
|
|
82
|
+
</template>
|
|
83
|
+
</Column>
|
|
84
|
+
<Column header="">
|
|
85
|
+
<template #body="{ data }">
|
|
86
|
+
<Button v-if="data.status === 'failed'" label="Retry" size="small" @click="retryEpisode(data)" />
|
|
87
|
+
</template>
|
|
88
|
+
</Column>
|
|
89
|
+
</DataTable>
|
|
90
|
+
<div v-else class="empty-content" style="padding:30px">
|
|
91
|
+
<h3>No {{ epFilter || '' }} episodes</h3>
|
|
92
|
+
</div>
|
|
93
|
+
</TabPanel>
|
|
94
|
+
</TabPanels>
|
|
95
|
+
</Tabs>
|
|
96
|
+
</template>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<script setup>
|
|
101
|
+
import { ref, computed, onMounted } from 'vue'
|
|
102
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
103
|
+
import { api } from '../api/client'
|
|
104
|
+
import { useDownloadStore } from '../stores/download'
|
|
105
|
+
import DataTable from 'primevue/datatable'
|
|
106
|
+
import Column from 'primevue/column'
|
|
107
|
+
import Tabs from 'primevue/tabs'
|
|
108
|
+
import TabList from 'primevue/tablist'
|
|
109
|
+
import Tab from 'primevue/tab'
|
|
110
|
+
import TabPanels from 'primevue/tabpanels'
|
|
111
|
+
import TabPanel from 'primevue/tabpanel'
|
|
112
|
+
|
|
113
|
+
const props = defineProps({ id: String })
|
|
114
|
+
const route = useRoute()
|
|
115
|
+
const router = useRouter()
|
|
116
|
+
const downloadStore = useDownloadStore()
|
|
117
|
+
|
|
118
|
+
const loading = ref(true)
|
|
119
|
+
const anime = ref(null)
|
|
120
|
+
const episodes = ref([])
|
|
121
|
+
const overrideQuality = ref(null)
|
|
122
|
+
const overrideLanguage = ref('sub')
|
|
123
|
+
const epFilter = ref('')
|
|
124
|
+
|
|
125
|
+
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"
|
|
126
|
+
|
|
127
|
+
function handleImgError(e) {
|
|
128
|
+
e.target.src = FALLBACK_POSTER
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const qualityOptions = [
|
|
132
|
+
{ label: 'Default', value: null },
|
|
133
|
+
{ label: '1080p', value: '1080p' },
|
|
134
|
+
{ label: '720p', value: '720p' },
|
|
135
|
+
{ label: '360p', value: '360p' },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
const stats = computed(() => {
|
|
139
|
+
const all = episodes.value
|
|
140
|
+
return {
|
|
141
|
+
done: all.filter(e => e.status === 'done').length,
|
|
142
|
+
failed: all.filter(e => e.status === 'failed').length,
|
|
143
|
+
pending: all.filter(e => e.status === 'pending' || e.status === 'downloading').length,
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const filteredEpisodes = computed(() => {
|
|
148
|
+
if (!epFilter.value) return episodes.value
|
|
149
|
+
return episodes.value.filter(e => e.status === epFilter.value)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
function statusSeverity(status) {
|
|
153
|
+
switch (status) {
|
|
154
|
+
case 'done': return 'success'
|
|
155
|
+
case 'failed': return 'danger'
|
|
156
|
+
case 'downloading': return 'info'
|
|
157
|
+
default: return 'secondary'
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function loadAnime() {
|
|
162
|
+
const id = props.id || route.params.id
|
|
163
|
+
try {
|
|
164
|
+
anime.value = await api.anime.get(id)
|
|
165
|
+
episodes.value = await api.anime.episodes(id)
|
|
166
|
+
overrideQuality.value = anime.value.override_quality || anime.value.quality || null
|
|
167
|
+
overrideLanguage.value = anime.value.language || 'sub'
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error(e)
|
|
170
|
+
} finally {
|
|
171
|
+
loading.value = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setQuality() {
|
|
176
|
+
api.anime.setQuality(anime.value.id, overrideQuality.value)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setLanguage() {
|
|
180
|
+
api.anime.setLanguage(anime.value.id, overrideLanguage.value)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function downloadAnime() {
|
|
184
|
+
await downloadStore.downloadAnime(anime.value.id, overrideQuality.value, overrideLanguage.value)
|
|
185
|
+
router.push('/download')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function toggleEnabled() {
|
|
189
|
+
const newState = !anime.value.enabled
|
|
190
|
+
await api.anime.setEnabled(anime.value.id, newState)
|
|
191
|
+
anime.value.enabled = newState
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function removeAnime() {
|
|
195
|
+
if (confirm(`Remove ${anime.value.title}?`)) {
|
|
196
|
+
await api.anime.remove(anime.value.id)
|
|
197
|
+
router.push('/anime')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
onMounted(loadAnime)
|
|
202
|
+
</script>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>Anime</h2>
|
|
5
|
+
<div class="subtitle">{{ filtered.length }} tracked</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="anime-list-toolbar">
|
|
9
|
+
<InputText v-model="search" placeholder="Filter by title..." style="flex:1;max-width:320px" />
|
|
10
|
+
<Select v-model="statusFilter" :options="statusOptions" placeholder="Status" style="width:140px" />
|
|
11
|
+
|
|
12
|
+
<Tabs v-model:value="epFilter" style="flex:1">
|
|
13
|
+
<TabList>
|
|
14
|
+
<Tab value="">All</Tab>
|
|
15
|
+
<Tab value="done">Downloaded</Tab>
|
|
16
|
+
<Tab value="failed">Failed</Tab>
|
|
17
|
+
<Tab value="pending">Pending</Tab>
|
|
18
|
+
</TabList>
|
|
19
|
+
</Tabs>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<Skeleton v-if="loading" width="100%" height="300px" />
|
|
23
|
+
|
|
24
|
+
<Card v-else-if="filtered.length === 0">
|
|
25
|
+
<template #content>
|
|
26
|
+
<div class="empty-content"><h3>No anime found</h3></div>
|
|
27
|
+
</template>
|
|
28
|
+
</Card>
|
|
29
|
+
|
|
30
|
+
<div v-else class="anime-grid">
|
|
31
|
+
<div v-for="a in filtered" :key="a.id" class="anime-card" @click="$router.push(`/anime/${a.id}`)">
|
|
32
|
+
<img v-if="a.poster_url" :src="a.poster_url" class="anime-poster" @error="handleImgError" />
|
|
33
|
+
<div class="anime-info">
|
|
34
|
+
<div class="anime-title">{{ a.anilist_title || a.title }}</div>
|
|
35
|
+
<div class="anime-meta">
|
|
36
|
+
{{ a.source }}
|
|
37
|
+
<Tag :value="a.confirmed_site_title ? 'matched' : 'unmatched'" :severity="a.confirmed_site_title ? 'success' : 'secondary'" size="small" style="margin-left:6px" />
|
|
38
|
+
</div>
|
|
39
|
+
<div class="anime-progress">
|
|
40
|
+
<div class="bar" :style="{ width: progressPercent(a) + '%' }"></div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="anime-stats">
|
|
43
|
+
<span class="done">{{ a.stats?.done || 0 }} done</span>
|
|
44
|
+
<span class="failed">{{ a.stats?.failed || 0 }} failed</span>
|
|
45
|
+
<span class="pending">{{ a.stats?.pending || 0 }} pending</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
import { ref, computed, onMounted } from 'vue'
|
|
55
|
+
import { api } from '../api/client'
|
|
56
|
+
import Tabs from 'primevue/tabs'
|
|
57
|
+
import TabList from 'primevue/tablist'
|
|
58
|
+
import Tab from 'primevue/tab'
|
|
59
|
+
|
|
60
|
+
const loading = ref(true)
|
|
61
|
+
const allAnime = ref([])
|
|
62
|
+
const search = ref('')
|
|
63
|
+
const statusFilter = ref('')
|
|
64
|
+
const epFilter = ref('')
|
|
65
|
+
|
|
66
|
+
const statusOptions = ['All', 'Matched', 'Unmatched']
|
|
67
|
+
|
|
68
|
+
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"
|
|
69
|
+
|
|
70
|
+
function handleImgError(e) {
|
|
71
|
+
e.target.src = FALLBACK_POSTER
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function progressPercent(a) {
|
|
75
|
+
const total = (a.stats?.done || 0) + (a.stats?.failed || 0) + (a.stats?.pending || 0)
|
|
76
|
+
if (!total) return 0
|
|
77
|
+
return Math.round(((a.stats?.done || 0) / total) * 100)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const filtered = computed(() => {
|
|
81
|
+
let list = allAnime.value
|
|
82
|
+
if (search.value) {
|
|
83
|
+
const s = search.value.toLowerCase()
|
|
84
|
+
list = list.filter((a) => (a.anilist_title || a.title).toLowerCase().includes(s))
|
|
85
|
+
}
|
|
86
|
+
if (statusFilter.value === 'Matched') {
|
|
87
|
+
list = list.filter((a) => a.confirmed_site_title)
|
|
88
|
+
} else if (statusFilter.value === 'Unmatched') {
|
|
89
|
+
list = list.filter((a) => !a.confirmed_site_title)
|
|
90
|
+
}
|
|
91
|
+
if (epFilter.value === 'done') {
|
|
92
|
+
list = list.filter((a) => (a.stats?.done || 0) > 0)
|
|
93
|
+
} else if (epFilter.value === 'failed') {
|
|
94
|
+
list = list.filter((a) => (a.stats?.failed || 0) > 0)
|
|
95
|
+
} else if (epFilter.value === 'pending') {
|
|
96
|
+
list = list.filter((a) => (a.stats?.pending || 0) > 0)
|
|
97
|
+
}
|
|
98
|
+
return list
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
onMounted(async () => {
|
|
102
|
+
try {
|
|
103
|
+
allAnime.value = await api.anime.list()
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(e)
|
|
106
|
+
} finally {
|
|
107
|
+
loading.value = false
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
</script>
|