diff --git a/ui/stores/SocketStore.js b/ui/stores/SocketStore.js deleted file mode 100644 index 3b9b628e..00000000 --- a/ui/stores/SocketStore.js +++ /dev/null @@ -1,163 +0,0 @@ -import { io } from "socket.io-client"; -import { ag } from "~/utils/index" - -export const useSocketStore = defineStore('socket', () => { - const runtimeConfig = useRuntimeConfig() - const config = useConfigStore() - const stateStore = useStateStore() - const toast = useNotification() - - const socket = ref(null) - const isConnected = ref(false) - - const connect = () => { - let opts = { - transports: ['websocket', 'polling'], - withCredentials: true, - } - - let url = runtimeConfig.public.wss - - if ('development' !== runtimeConfig.public?.APP_ENV) { - url = window.origin; - opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`; - } - - socket.value = io(url, opts) - - socket.value.on('connect', () => isConnected.value = true); - socket.value.on('disconnect', () => isConnected.value = false); - - socket.value.on('initial_data', stream => { - const initialData = JSON.parse(stream) - - config.setAll({ - app: initialData['config'], - tasks: initialData['tasks'], - folders: initialData['folders'], - presets: initialData['presets'], - paused: Boolean(initialData['paused']) - }) - - stateStore.addAll('queue', initialData['queue'] ?? {}) - stateStore.addAll('history', initialData['done'] ?? {}) - }) - - socket.value.on('added', stream => { - const item = JSON.parse(stream); - stateStore.add('queue', item._id, item); - toast.success(`Item queued: ${ag(stateStore.get('queue', item._id, {}), 'title')}`); - }); - - socket.value.on('error', stream => { - const json = JSON.parse(stream); - toast.error(`${json.data?.id ?? json?.type}: ${json?.message}`, json.data || {}); - }); - - socket.value.on('log_info', stream => { - const json = JSON.parse(stream); - toast.info(json?.message, json.data || {}); - }); - - socket.value.on('log_success', stream => { - const json = JSON.parse(stream); - toast.success(json?.message, json.data || {}); - }); - - socket.value.on('log_warning', stream => { - const json = JSON.parse(stream); - toast.warning(json?.message, json.data || {}); - }); - - socket.value.on('log_error', stream => { - const json = JSON.parse(stream); - toast.error(json?.message, json.data || {}); - }); - - socket.value.on('completed', stream => { - const item = JSON.parse(stream); - - if (true === stateStore.has('queue', item._id)) { - stateStore.remove('queue', item._id); - } - - stateStore.add('history', item._id, item); - }); - - socket.value.on('cancelled', stream => { - const item = JSON.parse(stream); - const id = item._id - - if (true !== stateStore.has('queue', id)) { - return - } - - toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {}), 'title')}`); - - if (true === stateStore.has('queue', id)) { - stateStore.remove('queue', id); - } - }); - - socket.value.on('cleared', stream => { - const item = JSON.parse(stream); - const id = item._id - - if (true !== stateStore.has('history', id)) { - return - } - - stateStore.remove('history', id); - }); - - socket.value.on("updated", stream => { - const data = JSON.parse(stream); - - if (true === stateStore.has('history', data._id)) { - stateStore.update('history', data._id, data); - return; - } - - let dl = stateStore.get('queue', data._id, {}); - data.deleting = dl?.deleting; - stateStore.update('queue', data._id, data); - }); - - socket.value.on("update", stream => { - const data = JSON.parse(stream); - if (true === stateStore.has('history', data._id)) { - stateStore.update('history', data._id, data); - return; - } - }); - - socket.value.on('paused', data => { - const json = JSON.parse(data); - const pausedState = Boolean(json.paused); - config.update('paused', pausedState); - - if (false === pausedState) { - toast.success('Download queue resumed.'); - return; - } - - toast.warning('Download queue paused.', { - timeout: 10000, - }); - }); - - socket.value.on('presets_update', data => config.update('presets', JSON.parse(data))); - } - - const on = (event, callback) => socket.value.on(event, callback); - const off = (event, callback) => socket.value.off(event, callback); - const emit = (event, data) => socket.value.emit(event, data); - - if (false === isConnected.value) { - connect(); - } - - window.ws = socket.value; - - return { connect, on, off, emit, socket, isConnected }; -}); diff --git a/ui/stores/SocketStore.ts b/ui/stores/SocketStore.ts new file mode 100644 index 00000000..a966595d --- /dev/null +++ b/ui/stores/SocketStore.ts @@ -0,0 +1,162 @@ +import { io } from "socket.io-client"; +import { ag } from "~/utils/index" +import type { Socket as IOSocket, SocketOptions } from "socket.io-client" +import type { ManagerOptions } from "socket.io-client"; + +export const useSocketStore = defineStore('socket', () => { + const runtimeConfig = useRuntimeConfig() + const config = useConfigStore() + const stateStore = useStateStore() + const toast = useNotification() + + const socket = ref(null) + const isConnected = ref(false) + + const connect = () => { + let opts = { + transports: ['websocket', 'polling'], + withCredentials: true, + } as Partial + + let url = runtimeConfig.public.wss + + if ('development' !== runtimeConfig.public?.APP_ENV) { + url = window.origin; + opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`; + } + + socket.value = io(url, opts) + + socket.value.on('connect', () => isConnected.value = true); + socket.value.on('disconnect', () => isConnected.value = false); + + socket.value.on('initial_data', stream => { + const json = JSON.parse(stream) + + config.setAll({ + app: json.data.config, + tasks: json.data.tasks, + folders: json.data.folders, + presets: json.data.presets, + paused: Boolean(json.data.paused) + }) + + stateStore.addAll('queue', json.data.queue || {}) + stateStore.addAll('history', json.data.done || {}) + }) + + socket.value.on('added', stream => { + const json = JSON.parse(stream); + stateStore.add('queue', json.data._id, json.data); + toast.success(`Item queued: ${ag(stateStore.get('queue', json.data._id, {}), 'title')}`); + }); + + socket.value.on('error', stream => { + const json = JSON.parse(stream); + toast.error(`${json.data?.id}: ${json?.message || json?.data?.message}`, json.data || {}); + }); + + ['log_info', 'log_success', 'log_warning', 'log_error'].forEach(event => socket.value?.on(event, stream => { + const json = JSON.parse(stream); + const message = json?.message || json?.data?.message; + const data = json.data?.data || json.data || {}; + switch (event) { + case 'log_info': + toast.info(message, data); + break; + case 'log_success': + toast.success(message, data); + break; + case 'log_warning': + toast.warning(message, data); + break; + case 'log_error': + toast.error(message, data); + break; + } + })); + + socket.value.on('completed', stream => { + const json = JSON.parse(stream); + + if (true === stateStore.has('queue', json.data._id)) { + stateStore.remove('queue', json.data._id); + } + + stateStore.add('history', json.data._id, json.data); + }); + + socket.value.on('cancelled', stream => { + const item = JSON.parse(stream); + const id = item.data._id + + if (true !== stateStore.has('queue', id)) { + return + } + + toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {}), 'title')}`); + + if (true === stateStore.has('queue', id)) { + stateStore.remove('queue', id); + } + }); + + socket.value.on('cleared', stream => { + const item = JSON.parse(stream); + const id = item.data._id + + if (true !== stateStore.has('history', id)) { + return + } + + stateStore.remove('history', id); + }); + + socket.value.on("updated", stream => { + const json = JSON.parse(stream); + const id = json.data._id; + + if (true === stateStore.has('history', id)) { + stateStore.update('history', id, json.data); + return; + } + + stateStore.update('queue', id, json.data); + }); + + socket.value.on("update", stream => { + const json = JSON.parse(stream); + if (true === stateStore.has('history', json.data._id)) { + stateStore.update('history', json.data._id, json.data); + return; + } + }); + + socket.value.on('paused', data => { + const json = JSON.parse(data); + const pausedState = Boolean(json.data.paused); + config.update('paused', pausedState); + + if (false === pausedState) { + toast.success('Download queue resumed.'); + return; + } + + toast.warning('Download queue paused.', { timeout: 10000 }); + }); + + socket.value.on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || [])); + } + + const emit = (event: string, data?: any): any => socket.value?.emit(event, data) + const on = (event: string, callback: (...args: any[]) => void): any => socket.value?.on(event, callback) + const off = (event: string, callback: (...args: any[]) => void): any => socket.value?.off(event, callback) + + if (false === isConnected.value) { + connect(); + } + + window.ws = socket.value; + + return { connect, on, off, emit, socket, isConnected }; +}); diff --git a/ui/stores/StateStore.js b/ui/stores/StateStore.js deleted file mode 100644 index ecf12c5a..00000000 --- a/ui/stores/StateStore.js +++ /dev/null @@ -1,43 +0,0 @@ -export const useStateStore = defineStore('state', () => { - const state = reactive({ - queue: {}, - history: {} - }); - - const actions = { - add(type, key, value) { - state[type][key] = value; - }, - update(type, key, value) { - state[type][key] = value; - }, - remove(type, key) { - if (state[type][key]) { - delete state[type][key]; - } - }, - get(type, key, defaultValue = null) { - return state[type][key] || defaultValue; - }, - has(type, key) { - return !!state[type][key]; - }, - clearAll(type) { - state[type] = {}; - }, - addAll(type, data) { - state[type] = data; - }, - move(fromType, toType, key) { - if (state[fromType][key]) { - state[toType][key] = state[fromType][key]; - delete state[fromType][key]; - } - }, - count(type) { - return Object.keys(state[type]).length; - } - } - - return { ...toRefs(state), ...actions }; -}); diff --git a/ui/stores/StateStore.ts b/ui/stores/StateStore.ts new file mode 100644 index 00000000..66487407 --- /dev/null +++ b/ui/stores/StateStore.ts @@ -0,0 +1,59 @@ +import { defineStore } from 'pinia' +import { reactive, toRefs } from 'vue' +import type { StoreItem } from '~/types/store' + +type StateType = 'queue' | 'history' +type KeyType = string +type ValueType = StoreItem + +interface State { + queue: Record + history: Record +} + +export const useStateStore = defineStore('state', () => { + const state = reactive({ queue: {}, history: {} }) + + const add = (type: StateType, key: KeyType, value: ValueType): void => { + state[type][key] = value + } + + const update = (type: StateType, key: KeyType, value: ValueType): void => { + state[type][key] = value + } + + const remove = (type: StateType, key: KeyType): void => { + if (state[type][key]) { + delete state[type][key] + } + } + + const get = (type: StateType, key: KeyType, defaultValue: ValueType | null | object = null): ValueType | null => { + return state[type][key] || defaultValue + } + + const has = (type: StateType, key: KeyType): boolean => { + return !!state[type][key] + } + + const clearAll = (type: StateType): void => { + state[type] = {} + } + + const addAll = (type: StateType, data: Record): void => { + state[type] = data + } + + const move = (fromType: StateType, toType: StateType, key: KeyType): void => { + if (state[fromType][key]) { + state[toType][key] = state[fromType][key] + delete state[fromType][key] + } + } + + const count = (type: StateType): number => { + return Object.keys(state[type]).length + } + + return { ...toRefs(state), add, update, remove, get, has, clearAll, addAll, move, count } +}) diff --git a/ui/types/sockets.d.ts b/ui/types/sockets.d.ts new file mode 100644 index 00000000..17b7dd8b --- /dev/null +++ b/ui/types/sockets.d.ts @@ -0,0 +1,8 @@ +export type Event = { + id: str + created_at: str + event: str + title: str | null = null + message: str | null = null + data: Any = { } +} diff --git a/ui/types/store.d.ts b/ui/types/store.d.ts new file mode 100644 index 00000000..be79749a --- /dev/null +++ b/ui/types/store.d.ts @@ -0,0 +1,76 @@ +export type ItemStatus = 'finished' | 'preparing' | 'error' | 'cancelled' | 'downloading' | 'postprocessing' | 'not_live' | null; +export type StoreItem = { + /** Unique identifier for the item */ + _id: string + /** Error message if available */ + error: string | null + /** Item source id */ + id: string + /** Title of the item */ + title: string + /** URL of the item */ + url: string + /** Preset used for the item */ + preset: string + /** Folder where the item is saved */ + folder: string + /** Download directory */ + download_dir: string + /** Temporary directory for the item */ + temp_dir: string + /** Status of the item */ + status: ItemStatus + /** If the item has cookies */ + cookies: string + /** If the item has custom output_template */ + template: string + /** If the item has custom output_template for chapters */ + template_chapter: string + /** When the item was created */ + timestamp: number + /** If the item is a live stream */ + is_live: boolean + /** ISO 8601 formatted start time of the item */ + datetime: string + /** Live stream start time if available */ + live_in: string | null + /** File size of the item if available */ + file_size: number | null + /** Custom yt-dlp command line arguments */ + cli: string + /** If the item is auto-started */ + auto_start: boolean + /** Options for the item */ + options: Record + /** Extras for the item */ + extras: { + /** Which channel the item belongs to */ + channel?: string + /** The video duration if available */ + duration?: number | null + /** The video release date if available */ + release_in?: string + /** The video thumbnail URL if available */ + thumbnail?: string + /** The uploader of the item if available */ + uploader?: string + } + /** The item temporary filename */ + tmpfilename?: string | null + /** The item filename */ + filename?: string | null + /** Actual file size of the item if available */ + total_bytes?: number | null + /** Estimated total bytes for the item if available */ + total_bytes_estimate?: number | null + /** Downloaded bytes of the item if available */ + downloaded_bytes?: number | null + /** Message attached with the item if available */ + msg?: string | null + /** Progress percentage of the item if available */ + percent?: number | null + /** Speed of the item download if available */ + speed?: number | null + /** Time remaining for the item download if available */ + eta?: number | null +}