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.
Files changed (89) hide show
  1. package/anime/LICENSE +21 -0
  2. package/anime/api/index.js +110 -0
  3. package/anime/app.js +79 -0
  4. package/anime/controllers/animeInfoController.js +48 -0
  5. package/anime/controllers/animeListController.js +21 -0
  6. package/anime/controllers/homeController.js +42 -0
  7. package/anime/controllers/playController.js +140 -0
  8. package/anime/controllers/queueController.js +20 -0
  9. package/anime/controllers/testController.js +667 -0
  10. package/anime/index.js +139 -0
  11. package/anime/middleware/cache.js +64 -0
  12. package/anime/middleware/errorHandler.js +33 -0
  13. package/anime/middleware/rateLimiter.js +73 -0
  14. package/anime/models/animeInfoModel.js +193 -0
  15. package/anime/models/animeListModel.js +69 -0
  16. package/anime/models/homeModel.js +33 -0
  17. package/anime/models/playModel.js +528 -0
  18. package/anime/models/queueModel.js +22 -0
  19. package/anime/package.json +27 -0
  20. package/anime/pnpm-lock.yaml +1774 -0
  21. package/anime/readme.md +236 -0
  22. package/anime/routes/animeInfoRoutes.js +9 -0
  23. package/anime/routes/animeListRoutes.js +9 -0
  24. package/anime/routes/homeRoutes.js +10 -0
  25. package/anime/routes/playRoutes.js +11 -0
  26. package/anime/routes/queueRoutes.js +8 -0
  27. package/anime/routes/testRoutes.js +325 -0
  28. package/anime/routes/webhookRoutes.js +23 -0
  29. package/anime/scrapers/animepahe.js +1053 -0
  30. package/anime/test-server.js +10 -0
  31. package/anime/test.sh +275 -0
  32. package/anime/utils/browser.js +172 -0
  33. package/anime/utils/config.js +209 -0
  34. package/anime/utils/dataProcessor.js +172 -0
  35. package/anime/utils/diskCache.js +228 -0
  36. package/anime/utils/flaresolverr.js +361 -0
  37. package/anime/utils/jsParser.js +19 -0
  38. package/anime/utils/redis.js +64 -0
  39. package/anime/utils/requestManager.js +706 -0
  40. package/anime/utils/urlConverter.js +88 -0
  41. package/anime/utils/webhookNotifier.js +190 -0
  42. package/cookiereader.py +291 -0
  43. package/package.json +14 -6
  44. package/src/anilist.js +15 -2
  45. package/src/api/anilist.js +32 -0
  46. package/src/api/anime.js +181 -0
  47. package/src/api/cf-bypass.js +131 -0
  48. package/src/api/config.js +134 -0
  49. package/src/api/daemon.js +62 -0
  50. package/src/api/download.js +98 -0
  51. package/src/api/match.js +58 -0
  52. package/src/api/runs.js +15 -0
  53. package/src/api/update.js +96 -0
  54. package/src/cli.js +18 -2
  55. package/src/config.js +14 -2
  56. package/src/daemon.js +1 -1
  57. package/src/db.js +38 -11
  58. package/src/engine.js +62 -11
  59. package/src/scraper.js +2 -2
  60. package/src/server.js +108 -0
  61. package/utils.js +21 -4
  62. package/web/index.html +13 -0
  63. package/web/package-lock.json +1420 -0
  64. package/web/package.json +24 -0
  65. package/web/src/App.vue +200 -0
  66. package/web/src/api/client.js +75 -0
  67. package/web/src/assets/styles/base.css +961 -0
  68. package/web/src/components/UpdateModal.vue +157 -0
  69. package/web/src/components/sidebar/SidebarResizable.vue +170 -0
  70. package/web/src/components/sidebar/sidebarConfig.js +24 -0
  71. package/web/src/main.js +104 -0
  72. package/web/src/router/index.js +44 -0
  73. package/web/src/stores/config.js +27 -0
  74. package/web/src/stores/download.js +107 -0
  75. package/web/src/stores/update.js +73 -0
  76. package/web/src/views/About.vue +42 -0
  77. package/web/src/views/AniListSync.vue +122 -0
  78. package/web/src/views/AnimeDetail.vue +202 -0
  79. package/web/src/views/AnimeList.vue +110 -0
  80. package/web/src/views/CFResult.vue +312 -0
  81. package/web/src/views/DaemonControl.vue +83 -0
  82. package/web/src/views/Dashboard.vue +187 -0
  83. package/web/src/views/DownloadCenter.vue +153 -0
  84. package/web/src/views/MatchFinder.vue +131 -0
  85. package/web/src/views/OAuthCallback.vue +44 -0
  86. package/web/src/views/Onboarding.vue +93 -0
  87. package/web/src/views/RunHistory.vue +122 -0
  88. package/web/src/views/Settings.vue +410 -0
  89. package/web/vite.config.js +23 -0
@@ -0,0 +1,157 @@
1
+ <template>
2
+ <Dialog
3
+ :visible="visible"
4
+ @update:visible="$emit('close')"
5
+ :header="title"
6
+ :modal="true"
7
+ :closable="!updateStore.installing"
8
+ :dismissableMask="!updateStore.installing"
9
+ style="width: 400px"
10
+ >
11
+ <div class="update-modal-body">
12
+ <div v-if="updateStore.installing" class="update-progress">
13
+ <ProgressBar :value="updateStore.installProgress" />
14
+ <p class="update-message">{{ updateStore.installMessage }}</p>
15
+ </div>
16
+
17
+ <div v-else-if="updateStore.installDone" class="update-done">
18
+ <p class="update-done-text">{{ updateStore.installMessage }}</p>
19
+ </div>
20
+
21
+ <div v-else-if="updateStore.installError" class="update-error">
22
+ <p class="update-error-text">{{ updateStore.installError }}</p>
23
+ </div>
24
+
25
+ <div v-else class="update-info">
26
+ <div class="update-versions">
27
+ <div class="update-version-row">
28
+ <span class="update-version-label">Current</span>
29
+ <span class="update-version-value">v{{ updateStore.currentVersion }}</span>
30
+ </div>
31
+ <div class="update-version-row">
32
+ <span class="update-version-label">Latest</span>
33
+ <span class="update-version-value update-version-latest">v{{ updateStore.latestVersion }}</span>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <template #footer>
40
+ <div class="update-modal-footer">
41
+ <Button
42
+ v-if="!updateStore.installing && !updateStore.installDone && !updateStore.installError"
43
+ label="Update"
44
+ icon="pi pi-download"
45
+ @click="install"
46
+ :disabled="updateStore.installing"
47
+ />
48
+ <Button
49
+ v-if="updateStore.installDone || updateStore.installError"
50
+ label="Close"
51
+ @click="$emit('close')"
52
+ />
53
+ </div>
54
+ </template>
55
+ </Dialog>
56
+ </template>
57
+
58
+ <script setup>
59
+ import Dialog from 'primevue/dialog'
60
+ import Button from 'primevue/button'
61
+ import ProgressBar from 'primevue/progressbar'
62
+ import { useUpdateStore } from '../stores/update'
63
+
64
+ defineProps({
65
+ visible: { type: Boolean, default: false },
66
+ })
67
+
68
+ defineEmits(['close'])
69
+
70
+ const updateStore = useUpdateStore()
71
+
72
+ const title = 'Update Available'
73
+
74
+ function install() {
75
+ updateStore.install()
76
+ }
77
+ </script>
78
+
79
+ <style scoped>
80
+ .update-modal-body {
81
+ padding: 8px 0;
82
+ }
83
+
84
+ .update-versions {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 8px;
88
+ }
89
+
90
+ .update-version-row {
91
+ display: flex;
92
+ justify-content: space-between;
93
+ align-items: center;
94
+ padding: 4px 0;
95
+ }
96
+
97
+ .update-version-label {
98
+ font-size: 13px;
99
+ color: var(--ink-700);
100
+ }
101
+
102
+ .update-version-value {
103
+ font-size: 13px;
104
+ font-weight: 600;
105
+ color: var(--text-primary);
106
+ font-family: var(--font-mono);
107
+ }
108
+
109
+ .update-version-latest {
110
+ color: var(--red-primary);
111
+ }
112
+
113
+ .update-progress {
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 12px;
117
+ }
118
+
119
+ .update-message {
120
+ font-size: 13px;
121
+ color: var(--text-dim);
122
+ text-align: center;
123
+ }
124
+
125
+ .update-done {
126
+ text-align: center;
127
+ padding: 12px 0;
128
+ }
129
+
130
+ .update-done-text {
131
+ font-size: 14px;
132
+ color: var(--status-done);
133
+ font-weight: 600;
134
+ }
135
+
136
+ .update-error {
137
+ text-align: center;
138
+ padding: 12px 0;
139
+ }
140
+
141
+ .update-error-text {
142
+ font-size: 13px;
143
+ color: var(--status-fail);
144
+ }
145
+
146
+ .update-modal-footer {
147
+ display: flex;
148
+ justify-content: flex-end;
149
+ gap: 8px;
150
+ }
151
+
152
+ .update-info {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 12px;
156
+ }
157
+ </style>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <aside
3
+ class="sidebar-resizable"
4
+ :class="{ collapsed, 'mobile-open': mobileOpen }"
5
+ :style="{ width: collapsed ? '56px' : width + 'px' }"
6
+ >
7
+ <div class="sidebar-resize-area" @mousedown.prevent="startResize"></div>
8
+
9
+ <div class="sidebar-logo" @dblclick="toggleCollapsed">
10
+ <span v-if="collapsed" class="slim-logo-text">A</span>
11
+ <template v-else>
12
+ <span class="sidebar-logo-brand">ANI-AUTO</span>
13
+ <span class="sidebar-logo-tagline">AUTOMATIC DOWNLOADER</span>
14
+ </template>
15
+ </div>
16
+
17
+ <nav class="sidebar-nav">
18
+ <router-link
19
+ v-for="item in navItems"
20
+ :key="item.href"
21
+ :to="item.href"
22
+ class="sidebar-nav-item"
23
+ :class="{ active: isActive(item) }"
24
+ @click="$emit('navigate')"
25
+ >
26
+ <span class="sidebar-nav-icon">
27
+ <HugeiconsIcon :icon="item.icon" :size="20" />
28
+ <span v-if="showBadge(item)" class="slim-badge"></span>
29
+ </span>
30
+ <span v-if="!collapsed" class="sidebar-nav-label">{{ item.label }}</span>
31
+ <span v-if="!collapsed && badgeValue(item)" class="sidebar-nav-count">{{ badgeValue(item) }}</span>
32
+ <span v-if="collapsed" class="slim-tooltip">{{ item.label }}</span>
33
+ </router-link>
34
+ </nav>
35
+
36
+ <div class="sidebar-footer" :style="{ display: 'flex', flexDirection: 'column' }">
37
+ <router-link
38
+ v-for="item in footerItems"
39
+ :key="item.href"
40
+ :to="item.href"
41
+ class="sidebar-nav-item"
42
+ :class="{ active: isActive(item) }"
43
+ @click="$emit('navigate')"
44
+ >
45
+ <span class="sidebar-nav-icon">
46
+ <HugeiconsIcon :icon="item.icon" :size="20" />
47
+ </span>
48
+ <span v-if="!collapsed" class="sidebar-nav-label">{{ item.label }}</span>
49
+ <span v-if="collapsed" class="slim-tooltip">{{ item.label }}</span>
50
+ </router-link>
51
+
52
+ <div class="sidebar-user">
53
+ <img
54
+ v-if="configStore.config?.anilistAvatar && !avatarError"
55
+ :src="configStore.config.anilistAvatar"
56
+ class="sidebar-avatar"
57
+ @error="avatarError = true"
58
+ />
59
+ <div v-else class="sidebar-avatar">{{ avatarInitial }}</div>
60
+ <div v-if="!collapsed" class="sidebar-user-info">
61
+ <div class="sidebar-user-name">{{ configStore.config?.anilistUsername || 'Not configured' }}</div>
62
+ <div class="sidebar-user-version">v1.4.4</div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </aside>
67
+ </template>
68
+
69
+ <script setup>
70
+ import { ref, computed, onBeforeUnmount } from 'vue'
71
+ import { useRoute } from 'vue-router'
72
+ import { useConfigStore } from '../../stores/config'
73
+ import { useDownloadStore } from '../../stores/download'
74
+ import { HugeiconsIcon } from '@hugeicons/vue'
75
+ import { navItems, footerItems } from './sidebarConfig'
76
+
77
+ const props = defineProps({
78
+ collapsed: { type: Boolean, default: false },
79
+ width: { type: Number, default: 200 },
80
+ mobileOpen: { type: Boolean, default: false },
81
+ })
82
+
83
+ const emit = defineEmits(['update:collapsed', 'update:width', 'navigate'])
84
+
85
+ const route = useRoute()
86
+ const configStore = useConfigStore()
87
+ const downloadStore = useDownloadStore()
88
+
89
+ const avatarInitial = computed(() => {
90
+ const name = configStore.config?.anilistUsername
91
+ return name ? name[0].toUpperCase() : '?'
92
+ })
93
+
94
+ const SNAP_THRESHOLD = 120
95
+ const MIN_EXPANDED = 200
96
+ const MAX_WIDTH = 400
97
+ const lastExpandedWidth = ref(props.width)
98
+
99
+ const avatarError = ref(false)
100
+ const resizing = ref(false)
101
+
102
+ function isActive(item) {
103
+ if (item.exact === false) return route.path.startsWith(item.href)
104
+ return route.path === item.href
105
+ }
106
+
107
+ function showBadge(item) {
108
+ return badgeValue(item) > 0
109
+ }
110
+
111
+ function badgeValue(item) {
112
+ if (item.badge?.type !== 'count') return 0
113
+ if (item.href === '/download') {
114
+ return downloadStore.running ? Object.keys(downloadStore.progress).length : 0
115
+ }
116
+ return 0
117
+ }
118
+
119
+ function toggleCollapsed() {
120
+ if (props.collapsed) {
121
+ emit('update:collapsed', false)
122
+ emit('update:width', lastExpandedWidth.value)
123
+ } else {
124
+ lastExpandedWidth.value = props.width
125
+ emit('update:collapsed', true)
126
+ emit('update:width', 56)
127
+ }
128
+ }
129
+
130
+ function startResize(e) {
131
+ resizing.value = true
132
+ document.body.style.cursor = 'col-resize'
133
+ document.body.style.userSelect = 'none'
134
+ window.addEventListener('mousemove', onMouseMove)
135
+ window.addEventListener('mouseup', stopResize)
136
+ }
137
+
138
+ function onMouseMove(e) {
139
+ if (!resizing.value) return
140
+ let newWidth = e.clientX
141
+ if (newWidth < SNAP_THRESHOLD) {
142
+ if (!props.collapsed) {
143
+ lastExpandedWidth.value = props.width
144
+ emit('update:collapsed', true)
145
+ emit('update:width', 56)
146
+ }
147
+ } else {
148
+ newWidth = Math.min(MAX_WIDTH, Math.max(MIN_EXPANDED, newWidth))
149
+ if (props.collapsed) {
150
+ emit('update:collapsed', false)
151
+ emit('update:width', lastExpandedWidth.value)
152
+ } else {
153
+ lastExpandedWidth.value = newWidth
154
+ emit('update:width', newWidth)
155
+ }
156
+ }
157
+ }
158
+
159
+ function stopResize() {
160
+ resizing.value = false
161
+ document.body.style.cursor = ''
162
+ document.body.style.userSelect = ''
163
+ window.removeEventListener('mousemove', onMouseMove)
164
+ window.removeEventListener('mouseup', stopResize)
165
+ }
166
+
167
+ onBeforeUnmount(() => {
168
+ stopResize()
169
+ })
170
+ </script>
@@ -0,0 +1,24 @@
1
+ import {
2
+ DashboardBrowsingIcon,
3
+ FolderLibraryIcon,
4
+ DownloadCircle01Icon,
5
+ HistoryIcon,
6
+ RefreshIcon,
7
+ ServerStackIcon,
8
+ SettingsIcon,
9
+ InformationCircleIcon,
10
+ } from '@hugeicons/core-free-icons'
11
+
12
+ export const navItems = [
13
+ { label: 'Dashboard', href: '/', icon: DashboardBrowsingIcon, exact: true },
14
+ { label: 'Anime', href: '/anime', icon: FolderLibraryIcon, exact: false },
15
+ { label: 'Download Center', href: '/download', icon: DownloadCircle01Icon, exact: true, badge: { type: 'count' } },
16
+ { label: 'Run History', href: '/runs', icon: HistoryIcon, exact: true },
17
+ { label: 'AniList Sync', href: '/anilist', icon: RefreshIcon, exact: true },
18
+ { label: 'Daemon', href: '/daemon', icon: ServerStackIcon, exact: true },
19
+ ]
20
+
21
+ export const footerItems = [
22
+ { label: 'Settings', href: '/settings', icon: SettingsIcon, exact: true },
23
+ { label: 'About', href: '/about', icon: InformationCircleIcon, exact: true },
24
+ ]
@@ -0,0 +1,104 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import App from './App.vue'
4
+ import router from './router'
5
+ import PrimeVue from 'primevue/config'
6
+ import Aura from '@primeuix/themes/aura'
7
+ import { definePreset } from '@primeuix/themes'
8
+ import ToastService from 'primevue/toastservice'
9
+ import Tooltip from 'primevue/tooltip'
10
+
11
+ import Button from 'primevue/button'
12
+ import Card from 'primevue/card'
13
+ import Badge from 'primevue/badge'
14
+ import Tag from 'primevue/tag'
15
+ import InputText from 'primevue/inputtext'
16
+ import Select from 'primevue/select'
17
+ import Checkbox from 'primevue/checkbox'
18
+ import Avatar from 'primevue/avatar'
19
+ import Skeleton from 'primevue/skeleton'
20
+ import Toast from 'primevue/toast'
21
+ import ProgressBar from 'primevue/progressbar'
22
+ import InputNumber from 'primevue/inputnumber'
23
+ import Textarea from 'primevue/textarea'
24
+ import ToggleSwitch from 'primevue/toggleswitch'
25
+
26
+ import './assets/styles/base.css'
27
+
28
+ const MyPreset = definePreset(Aura, {
29
+ semantic: {
30
+ primary: {
31
+ 50: '#fef2f2',
32
+ 100: '#fee2e2',
33
+ 200: '#fecaca',
34
+ 300: '#fca5a5',
35
+ 400: '#f87171',
36
+ 500: '#ef4444',
37
+ 600: '#cc0000',
38
+ 700: '#990000',
39
+ 800: '#7f0000',
40
+ 900: '#660000',
41
+ 950: '#4d0000',
42
+ color: '{primary.600}',
43
+ contrastColor: '#ffffff',
44
+ hoverColor: '{primary.700}',
45
+ activeColor: '{primary.800}',
46
+ },
47
+ colorScheme: {
48
+ dark: {
49
+ surface: {
50
+ 0: '#ffffff',
51
+ 50: '#f5f5f5',
52
+ 100: '#e5e5e5',
53
+ 200: '#cccccc',
54
+ 300: '#999999',
55
+ 400: '#666666',
56
+ 500: '#555555',
57
+ 600: '#444444',
58
+ 700: '#333333',
59
+ 800: '#1a1a1a',
60
+ 900: '#111111',
61
+ 950: '#0a0a0a',
62
+ },
63
+ primary: {
64
+ color: '{primary.600}',
65
+ contrastColor: '#ffffff',
66
+ hoverColor: '{primary.700}',
67
+ activeColor: '{primary.800}',
68
+ },
69
+ },
70
+ },
71
+ },
72
+ })
73
+
74
+ const app = createApp(App)
75
+ app.use(createPinia())
76
+ app.use(router)
77
+ app.use(PrimeVue, {
78
+ theme: {
79
+ preset: MyPreset,
80
+ options: {
81
+ darkModeSelector: '.dark',
82
+ },
83
+ },
84
+ ripple: true,
85
+ })
86
+ app.use(ToastService)
87
+ app.directive('tooltip', Tooltip)
88
+
89
+ app.component('Button', Button)
90
+ app.component('Card', Card)
91
+ app.component('Badge', Badge)
92
+ app.component('Tag', Tag)
93
+ app.component('InputText', InputText)
94
+ app.component('Select', Select)
95
+ app.component('Checkbox', Checkbox)
96
+ app.component('Avatar', Avatar)
97
+ app.component('Skeleton', Skeleton)
98
+ app.component('Toast', Toast)
99
+ app.component('ProgressBar', ProgressBar)
100
+ app.component('InputNumber', InputNumber)
101
+ app.component('ToggleSwitch', ToggleSwitch)
102
+ app.component('Textarea', Textarea)
103
+
104
+ app.mount('#app')
@@ -0,0 +1,44 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ import { api } from '../api/client'
3
+
4
+ const routes = [
5
+ { path: '/onboarding', name: 'onboarding', meta: { layout: 'full' }, component: () => import('../views/Onboarding.vue') },
6
+ { path: '/oauth/callback', name: 'oauth-callback', meta: { layout: 'full' }, component: () => import('../views/OAuthCallback.vue') },
7
+ { path: '/', name: 'dashboard', component: () => import('../views/Dashboard.vue') },
8
+ { path: '/anime', name: 'anime', component: () => import('../views/AnimeList.vue') },
9
+ { path: '/anime/:id', name: 'anime-detail', component: () => import('../views/AnimeDetail.vue'), props: true },
10
+ { path: '/anilist', name: 'anilist-sync', component: () => import('../views/AniListSync.vue') },
11
+ { path: '/match/:animeId', name: 'match', component: () => import('../views/MatchFinder.vue'), props: true },
12
+ { path: '/download', name: 'download', component: () => import('../views/DownloadCenter.vue') },
13
+ { path: '/cf-result', name: 'cf-result', meta: { layout: 'full' }, component: () => import('../views/CFResult.vue') },
14
+ { path: '/settings', name: 'settings', component: () => import('../views/Settings.vue') },
15
+ { path: '/daemon', name: 'daemon', component: () => import('../views/DaemonControl.vue') },
16
+ { path: '/runs', name: 'runs', component: () => import('../views/RunHistory.vue') },
17
+ { path: '/about', name: 'about', component: () => import('../views/About.vue') },
18
+ ]
19
+
20
+ const router = createRouter({
21
+ history: createWebHistory(),
22
+ routes,
23
+ })
24
+
25
+ router.beforeEach(async (to, from, next) => {
26
+ if (to.name === 'oauth-callback') return next()
27
+ try {
28
+ const config = await api.config.get()
29
+ if (to.name === 'onboarding') {
30
+ if (config.anilistUsername || config.onboardingSkipped) {
31
+ return next({ name: 'dashboard' })
32
+ }
33
+ return next()
34
+ }
35
+ if (!config.anilistUsername && !config.manualList?.length && !config.onboardingSkipped) {
36
+ return next({ name: 'onboarding' })
37
+ }
38
+ } catch {
39
+ return next({ name: 'onboarding' })
40
+ }
41
+ next()
42
+ })
43
+
44
+ export default router
@@ -0,0 +1,27 @@
1
+ import { defineStore } from 'pinia'
2
+ import { api } from '../api/client'
3
+
4
+ export const useConfigStore = defineStore('config', {
5
+ state: () => ({
6
+ config: null,
7
+ loading: false,
8
+ }),
9
+ getters: {
10
+ isConfigured: (state) => !!state.config?.anilistUsername,
11
+ },
12
+ actions: {
13
+ async fetch() {
14
+ this.loading = true
15
+ try {
16
+ this.config = await api.config.get()
17
+ } catch (e) {
18
+ console.error('config fetch failed', e)
19
+ } finally {
20
+ this.loading = false
21
+ }
22
+ },
23
+ async update(data) {
24
+ this.config = await api.config.update(data)
25
+ },
26
+ },
27
+ })
@@ -0,0 +1,107 @@
1
+ import { defineStore } from 'pinia'
2
+ import { api, createWebSocket } from '../api/client'
3
+
4
+ export const useDownloadStore = defineStore('download', {
5
+ state: () => ({
6
+ running: false,
7
+ queue: [],
8
+ progress: {},
9
+ summary: null,
10
+ logs: [],
11
+ ws: null,
12
+ onEvent: null,
13
+ }),
14
+ actions: {
15
+ connectWebSocket() {
16
+ if (this.ws) return
17
+ this.ws = createWebSocket()
18
+ this.ws.onmessage = (event) => {
19
+ try {
20
+ const msg = JSON.parse(event.data)
21
+ this.handleEvent(msg)
22
+ } catch (e) {}
23
+ }
24
+ this.ws.onclose = () => {
25
+ this.ws = null
26
+ setTimeout(() => this.connectWebSocket(), 3000)
27
+ }
28
+ },
29
+ handleEvent(msg) {
30
+ switch (msg.type) {
31
+ case 'queue:update':
32
+ this.queue = msg.queue
33
+ break
34
+ case 'anime:start':
35
+ this.progress[msg.animeId] = { title: msg.title, episodes: [], done: 0, failed: 0 }
36
+ break
37
+ case 'episode:start':
38
+ this.running = true
39
+ break
40
+ case 'episode:progress':
41
+ if (!this.progress[msg.animeId]) break
42
+ this.progress[msg.animeId].episodes[msg.episode] = {
43
+ percent: msg.percent,
44
+ speed: msg.speed,
45
+ downloaded: msg.downloaded,
46
+ total: msg.total,
47
+ }
48
+ break
49
+ case 'episode:done':
50
+ if (this.progress[msg.animeId]) {
51
+ this.progress[msg.animeId].done++
52
+ this.progress[msg.animeId].episodes[msg.episode] = { percent: 100, done: true }
53
+ }
54
+ break
55
+ case 'episode:failed':
56
+ if (this.progress[msg.animeId]) {
57
+ this.progress[msg.animeId].failed++
58
+ this.progress[msg.animeId].episodes[msg.episode] = { error: msg.error }
59
+ }
60
+ break
61
+ case 'anime:complete':
62
+ break
63
+ case 'log':
64
+ this.logs.push({ level: msg.level, message: msg.message, timestamp: msg.timestamp })
65
+ if (this.logs.length > 500) this.logs.shift()
66
+ break
67
+ case 'run:complete':
68
+ this.summary = msg
69
+ this.running = false
70
+ break
71
+ case 'run:error':
72
+ this.running = false
73
+ break
74
+ }
75
+ if (this.onEvent) this.onEvent(msg)
76
+ },
77
+ async startDownload(quality, language) {
78
+ this.running = true
79
+ this.progress = {}
80
+ this.summary = null
81
+ this.logs = []
82
+ try {
83
+ await api.download.run({ quality, language })
84
+ } catch {
85
+ this.running = false
86
+ }
87
+ },
88
+ async downloadAnime(id, quality, language) {
89
+ this.running = true
90
+ try {
91
+ await api.download.anime(id, { quality, language })
92
+ } catch {
93
+ this.running = false
94
+ }
95
+ },
96
+ async retryFailed() {
97
+ await api.download.retry()
98
+ },
99
+ reset() {
100
+ this.running = false
101
+ this.queue = []
102
+ this.progress = {}
103
+ this.summary = null
104
+ this.logs = []
105
+ },
106
+ },
107
+ })