Merge pull request #334 from arabcoders/dev
Make it possible to cancel download and keep the downloaded tmp data
This commit is contained in:
commit
f4ec06cef9
22 changed files with 614 additions and 539 deletions
|
|
@ -4,18 +4,20 @@ import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import signal
|
||||||
import time
|
import time
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import yt_dlp
|
import yt_dlp.utils
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .Events import EventBus, Events
|
from .Events import EventBus, Events
|
||||||
from .ffprobe import ffprobe
|
from .ffprobe import ffprobe
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
from .Utils import delete_dir, extract_info, extract_ytdlp_logs, load_cookies
|
from .Utils import delete_dir, extract_info, extract_ytdlp_logs, load_cookies
|
||||||
|
from .ytdlp import YTDLP
|
||||||
from .YTDLPOpts import YTDLPOpts
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -264,10 +266,17 @@ class Download:
|
||||||
|
|
||||||
params["logger"] = NestedLogger(self.logger)
|
params["logger"] = NestedLogger(self.logger)
|
||||||
|
|
||||||
cls = yt_dlp.YoutubeDL(params=params)
|
cls = YTDLP(params=params)
|
||||||
|
|
||||||
self.started_time = int(time.time())
|
self.started_time = int(time.time())
|
||||||
|
|
||||||
|
def mark_cancelled(*_):
|
||||||
|
cls._interrupted = True
|
||||||
|
cls.to_screen("[info] Interrupt received, exiting cleanly...")
|
||||||
|
raise SystemExit(130) # noqa: TRY301
|
||||||
|
|
||||||
|
signal.signal(signal.SIGUSR1, mark_cancelled)
|
||||||
|
|
||||||
if isinstance(self.info_dict, dict) and len(self.info_dict) > 1:
|
if isinstance(self.info_dict, dict) and len(self.info_dict) > 1:
|
||||||
self.logger.debug(f"Downloading '{self.info.url}' using pre-info.")
|
self.logger.debug(f"Downloading '{self.info.url}' using pre-info.")
|
||||||
cls.process_ie_result(
|
cls.process_ie_result(
|
||||||
|
|
@ -275,10 +284,10 @@ class Download:
|
||||||
download=True,
|
download=True,
|
||||||
extra_info={k: v for k, v in self.info.extras.items() if k not in self.info_dict},
|
extra_info={k: v for k, v in self.info.extras.items() if k not in self.info_dict},
|
||||||
)
|
)
|
||||||
ret = cls._download_retcode
|
ret: int = cls._download_retcode
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f"Downloading using url: {self.info.url}")
|
self.logger.debug(f"Downloading using url: {self.info.url}")
|
||||||
ret = cls.download(url_list=[self.info.url])
|
ret: int = cls.download(url_list=[self.info.url])
|
||||||
|
|
||||||
self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"})
|
self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"})
|
||||||
except yt_dlp.utils.ExistingVideoReached as exc:
|
except yt_dlp.utils.ExistingVideoReached as exc:
|
||||||
|
|
@ -333,7 +342,7 @@ class Download:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.cancel_in_progress = True
|
self.cancel_in_progress = True
|
||||||
procId = self.proc.ident
|
procId: int | None = self.proc.ident
|
||||||
|
|
||||||
self.logger.info(f"Closing PID='{procId}' download process.")
|
self.logger.info(f"Closing PID='{procId}' download process.")
|
||||||
|
|
||||||
|
|
@ -387,7 +396,10 @@ class Download:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"Killing download process: '{self.proc.ident}'.")
|
self.logger.info(f"Killing download process: '{self.proc.ident}'.")
|
||||||
self.proc.kill()
|
if self.proc.pid and "posix" == os.name:
|
||||||
|
os.kill(self.proc.pid, signal.SIGUSR1)
|
||||||
|
else:
|
||||||
|
self.proc.kill()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to kill process: '{self.proc.ident}'. {e}")
|
self.logger.error(f"Failed to kill process: '{self.proc.ident}'. {e}")
|
||||||
|
|
@ -398,9 +410,9 @@ class Download:
|
||||||
if self.temp_keep is True or not self.temp_path:
|
if self.temp_keep is True or not self.temp_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
if "finished" != self.info.status and self.is_live:
|
if "finished" != self.info.status and self.info.downloaded_bytes > 0:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"Keeping live temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'."
|
f"Keeping temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from pathlib import Path
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import yt_dlp
|
import yt_dlp.utils
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .ag_utils import ag
|
from .ag_utils import ag
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ from http.cookiejar import MozillaCookieJar
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
import yt_dlp
|
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from yt_dlp.utils import age_restricted, match_str
|
from yt_dlp.utils import age_restricted, match_str
|
||||||
|
|
||||||
from .LogWrapper import LogWrapper
|
from .LogWrapper import LogWrapper
|
||||||
|
from .ytdlp import YTDLP
|
||||||
|
|
||||||
LOG: logging.Logger = logging.getLogger("Utils")
|
LOG: logging.Logger = logging.getLogger("Utils")
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ REMOVE_KEYS: list = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
|
YTDLP_INFO_CLS: YTDLP = None
|
||||||
|
|
||||||
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
||||||
|
|
||||||
|
|
@ -189,7 +189,7 @@ def extract_info(
|
||||||
if no_archive and "download_archive" in params:
|
if no_archive and "download_archive" in params:
|
||||||
del params["download_archive"]
|
del params["download_archive"]
|
||||||
|
|
||||||
data = yt_dlp.YoutubeDL(params=params).extract_info(url, download=False)
|
data = YTDLP(params=params).extract_info(url, download=False)
|
||||||
|
|
||||||
if data and follow_redirect and "_type" in data and "url" == data["_type"]:
|
if data and follow_redirect and "_type" in data and "url" == data["_type"]:
|
||||||
return extract_info(
|
return extract_info(
|
||||||
|
|
@ -208,7 +208,7 @@ def extract_info(
|
||||||
if not data["is_premiere"]:
|
if not data["is_premiere"]:
|
||||||
data["is_premiere"] = "video" == data.get("media_type") and "is_upcoming" == data.get("live_status")
|
data["is_premiere"] = "video" == data.get("media_type") and "is_upcoming" == data.get("live_status")
|
||||||
|
|
||||||
return yt_dlp.YoutubeDL.sanitize_info(data) if sanitize_info else data
|
return YTDLP.sanitize_info(data) if sanitize_info else data
|
||||||
|
|
||||||
|
|
||||||
def merge_dict(source: dict, destination: dict) -> dict:
|
def merge_dict(source: dict, destination: dict) -> dict:
|
||||||
|
|
@ -1103,7 +1103,7 @@ def get_archive_id(url: str) -> tuple[bool, dict[str | None, str | None, str | N
|
||||||
}
|
}
|
||||||
|
|
||||||
if YTDLP_INFO_CLS is None:
|
if YTDLP_INFO_CLS is None:
|
||||||
YTDLP_INFO_CLS = yt_dlp.YoutubeDL(
|
YTDLP_INFO_CLS = YTDLP(
|
||||||
params={
|
params={
|
||||||
"color": "no_color",
|
"color": "no_color",
|
||||||
"extract_flat": True,
|
"extract_flat": True,
|
||||||
|
|
|
||||||
12
app/library/ytdlp.py
Normal file
12
app/library/ytdlp.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
|
||||||
|
class YTDLP(yt_dlp.YoutubeDL):
|
||||||
|
_interrupted = False
|
||||||
|
|
||||||
|
def _delete_downloaded_files(self, *args, **kwargs):
|
||||||
|
if self._interrupted:
|
||||||
|
self.to_screen("[info] Cancelled — skipping temp cleanup.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return super()._delete_downloaded_files(*args, **kwargs)
|
||||||
12
ui/@types/conditions.d.ts
vendored
Normal file
12
ui/@types/conditions.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type ConditionItem = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
filter: string
|
||||||
|
cli: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImportedConditionItem = ConditionItem & {
|
||||||
|
_type: string
|
||||||
|
_version: string
|
||||||
|
}
|
||||||
20
ui/@types/item.d.ts
vendored
Normal file
20
ui/@types/item.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export type item_request = {
|
||||||
|
/** Unique identifier for the item */
|
||||||
|
id?: string|null,
|
||||||
|
/** URL of the item to download */
|
||||||
|
url: string,
|
||||||
|
/** Preset to use for the download */
|
||||||
|
preset?: string,
|
||||||
|
/** Where to save the downloaded item */
|
||||||
|
folder?: string,
|
||||||
|
/** Output template for the downloaded item */
|
||||||
|
template?: string,
|
||||||
|
/** Additional command line options for yt-dlp */
|
||||||
|
cli?: string,
|
||||||
|
/** Cookies file for the download */
|
||||||
|
cookies?: string,
|
||||||
|
/** Auto start the download */
|
||||||
|
auto_start?: boolean,
|
||||||
|
/** Extras data for the item */
|
||||||
|
extras?: Record<string, any>,
|
||||||
|
}
|
||||||
18
ui/@types/responses.d.ts
vendored
Normal file
18
ui/@types/responses.d.ts
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export type error_response = {
|
||||||
|
/** The error message */
|
||||||
|
error: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type convert_args_response = {
|
||||||
|
/** The converted CLI args */
|
||||||
|
opts?: Record<string, any>,
|
||||||
|
/** The output template if was provided */
|
||||||
|
output_template?: string,
|
||||||
|
/** The download path if was provided */
|
||||||
|
download_path?: string,
|
||||||
|
/** The download format if was provided */
|
||||||
|
format?: string,
|
||||||
|
/** The removed options from the original CLI args if any. */
|
||||||
|
removed_options?: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -206,77 +206,37 @@
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
const emitter = defineEmits(['cancel', 'submit'])
|
|
||||||
import { decode } from '~/utils/importer'
|
import { decode } from '~/utils/importer'
|
||||||
|
import type { ConditionItem, ImportedConditionItem } from '~/@types/conditions'
|
||||||
|
|
||||||
const props = defineProps({
|
const emitter = defineEmits<{
|
||||||
reference: {
|
(e: 'cancel'): void
|
||||||
type: String,
|
(e: 'submit', payload: { reference: string | null | undefined, item: ConditionItem }): void
|
||||||
required: false,
|
}>()
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
addInProgress: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const test_data = ref({ show: false, url: '', data: { status: null, data: {} }, in_progress: false })
|
const props = defineProps<{
|
||||||
|
reference?: string | null
|
||||||
|
item: ConditionItem
|
||||||
|
addInProgress?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
const box = useConfirm()
|
|
||||||
const form = reactive(JSON.parse(JSON.stringify(props.item)))
|
|
||||||
const import_string = ref('')
|
|
||||||
const showImport = useStorage('showImport', false)
|
const showImport = useStorage('showImport', false)
|
||||||
|
const box = useConfirm()
|
||||||
|
|
||||||
const run_test = async () => {
|
const form = reactive<ConditionItem>(JSON.parse(JSON.stringify(props.item)))
|
||||||
if (!test_data.value.url) {
|
const import_string = ref('')
|
||||||
toast.error('The URL is required for testing.', { force: true })
|
const test_data = ref<{
|
||||||
return
|
show: boolean,
|
||||||
}
|
url: string,
|
||||||
|
in_progress: boolean,
|
||||||
|
data: { status: boolean | null, data: Record<string, any> }
|
||||||
|
}>({ show: false, url: '', in_progress: false, data: { status: null, data: {} } })
|
||||||
|
|
||||||
try {
|
const checkInfo = async (): Promise<void> => {
|
||||||
new URL(test_data.value.url)
|
const required: (keyof ConditionItem)[] = ['name', 'filter', 'cli']
|
||||||
} catch (e) {
|
|
||||||
toast.error('The URL is invalid.', { force: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
test_data.value.in_progress = true
|
|
||||||
test_data.value.data.status = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request('/api/conditions/test', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
url: test_data.value.url,
|
|
||||||
condition: form.filter,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const json = await response.json()
|
|
||||||
if (!response.ok) {
|
|
||||||
toast.error(json.message || json.error || 'Unknown error', { force: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
test_data.value.data = json
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`Failed to test condition. ${error.message}`)
|
|
||||||
} finally {
|
|
||||||
test_data.value.in_progress = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkInfo = async () => {
|
|
||||||
const required = ['name', 'filter', 'cli']
|
|
||||||
|
|
||||||
for (const key of required) {
|
for (const key of required) {
|
||||||
if (!form[key]) {
|
if (!form[key]) {
|
||||||
|
|
@ -285,15 +245,15 @@ const checkInfo = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form?.cli && '' !== form.cli) {
|
if (form.cli && '' !== form.cli.trim()) {
|
||||||
const options = await convertOptions(form.cli)
|
const options = await convertOptions(form.cli)
|
||||||
if (null === options) {
|
if (options === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.cli = form.cli.trim()
|
form.cli = form.cli.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
let copy = JSON.parse(JSON.stringify(form))
|
const copy: ConditionItem = JSON.parse(JSON.stringify(form))
|
||||||
|
|
||||||
for (const key in copy) {
|
for (const key in copy) {
|
||||||
if (typeof copy[key] !== 'string') {
|
if (typeof copy[key] !== 'string') {
|
||||||
|
|
@ -305,32 +265,68 @@ const checkInfo = async () => {
|
||||||
emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) })
|
emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) })
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertOptions = async args => {
|
const convertOptions = async (args: string): Promise<any | null> => {
|
||||||
try {
|
try {
|
||||||
const response = await convertCliOptions(args)
|
const response = await convertCliOptions(args)
|
||||||
return response.opts
|
return response.opts
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const importItem = async () => {
|
const run_test = async (): Promise<void> => {
|
||||||
let val = import_string.value.trim()
|
if (!test_data.value.url) {
|
||||||
|
toast.error('The URL is required for testing.', { force: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(test_data.value.url)
|
||||||
|
} catch {
|
||||||
|
toast.error('The URL is invalid.', { force: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
test_data.value.in_progress = true
|
||||||
|
test_data.value.data.status = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request('/api/conditions/test', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ url: test_data.value.url, condition: form.filter })
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
if (!response.ok) {
|
||||||
|
toast.error(json.message || json.error || 'Unknown error', { force: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
test_data.value.data = json
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(`Failed to test condition. ${error.message}`)
|
||||||
|
} finally {
|
||||||
|
test_data.value.in_progress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importItem = async (): Promise<void> => {
|
||||||
|
const val = import_string.value.trim()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
toast.error('The import string is required.')
|
toast.error('The import string is required.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = decode(val)
|
const item = decode(val) as ImportedConditionItem
|
||||||
|
|
||||||
if (!item?._type || 'condition' !== item._type) {
|
if (!item?._type || item._type !== 'condition') {
|
||||||
toast.error(`Invalid import string. Expected type 'condition', got '${item._type ?? 'unknown'}'.`)
|
toast.error(`Invalid import string. Expected type 'condition', got '${item._type ?? 'unknown'}'.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((form.filter || form.cli) && false === box.confirm('This will overwrite the current data. Are you sure?', true)) {
|
if ((form.filter || form.cli) && !box.confirm('Overwrite the current form fields?', true)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,14 +344,14 @@ const importItem = async () => {
|
||||||
|
|
||||||
import_string.value = ''
|
import_string.value = ''
|
||||||
showImport.value = false
|
showImport.value = false
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.error(`Failed to parse import string. ${e.message}`)
|
toast.error(`Failed to parse import string. ${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const show_data = () => {
|
const show_data = (): string => {
|
||||||
if (!test_data.value.data?.data || !Object.keys(test_data.value.data?.data).length) {
|
if (!test_data.value.data?.data || Object.keys(test_data.value.data.data).length === 0) {
|
||||||
return 'No data to show.'
|
return 'No data to show.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,40 +19,32 @@
|
||||||
</vTooltip>
|
</vTooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { request } from '~/utils/index'
|
import { request } from '~/utils/index'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps < {
|
||||||
image: {
|
image?: string
|
||||||
type: String,
|
title?: string
|
||||||
required: false,
|
loader?: () => Promise < void>
|
||||||
},
|
privacy ?: boolean
|
||||||
title: {
|
} > ()
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
loader: {
|
|
||||||
Type: Function,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
privacy: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cache = useSessionCache()
|
const cache = useSessionCache()
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
const url = ref()
|
const url = ref < string | null > (null)
|
||||||
const error = ref(false)
|
const error = ref(false)
|
||||||
const isPreloading = ref(false)
|
const isPreloading = ref(false)
|
||||||
|
|
||||||
let loadTimer = null;
|
let loadTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const cancelRequest = new AbortController();
|
const cancelRequest = new AbortController()
|
||||||
|
|
||||||
const defaultLoader = async () => {
|
const defaultLoader = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
if (!props.image) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (cache.has(props.image)) {
|
if (cache.has(props.image)) {
|
||||||
url.value = cache.get(props.image)
|
url.value = cache.get(props.image)
|
||||||
return
|
return
|
||||||
|
|
@ -60,18 +52,16 @@ const defaultLoader = async () => {
|
||||||
|
|
||||||
const response = await request(props.image, { signal: cancelRequest.signal })
|
const response = await request(props.image, { signal: cancelRequest.signal })
|
||||||
|
|
||||||
if (200 !== response.status) {
|
if (!response.ok) {
|
||||||
toast.error(`ImageView Request error. ${response.status}: ${response.statusText}`)
|
toast.error(`ImageView Request error. ${response.status}: ${response.statusText}`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const objUrl = URL.createObjectURL(await response.blob());
|
const objUrl = URL.createObjectURL(await response.blob())
|
||||||
|
|
||||||
cache.set(props.image, objUrl)
|
cache.set(props.image, objUrl)
|
||||||
|
url.value = objUrl
|
||||||
url.value = objUrl;
|
} catch (e: any) {
|
||||||
} catch (e) {
|
if (e === 'not_needed') {
|
||||||
if ('not_needed' === e) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
@ -81,25 +71,28 @@ const defaultLoader = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopTimer = async () => {
|
const stopTimer = async (): Promise<void> => {
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.value) {
|
if (url.value) {
|
||||||
isPreloading.value = false
|
isPreloading.value = false
|
||||||
url.value = null;
|
url.value = null
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await awaiter(() => isPreloading.value)
|
await awaiter(() => isPreloading.value)
|
||||||
clearTimeout(loadTimer)
|
if (loadTimer !== null) {
|
||||||
|
clearTimeout(loadTimer)
|
||||||
|
}
|
||||||
|
|
||||||
isPreloading.value = false
|
isPreloading.value = false
|
||||||
url.value = null;
|
url.value = null
|
||||||
cancelRequest.abort('not_needed')
|
cancelRequest.abort('not_needed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async (): Promise<void> => {
|
||||||
if (props.loader) {
|
if (props.loader) {
|
||||||
return props.loader()
|
return props.loader()
|
||||||
}
|
}
|
||||||
|
|
@ -107,11 +100,18 @@ const loadContent = async () => {
|
||||||
return defaultLoader()
|
return defaultLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearCache = async () => {
|
const clearCache = async (): Promise<void> => {
|
||||||
|
if (!props.image) return
|
||||||
|
|
||||||
cache.remove(props.image)
|
cache.remove(props.image)
|
||||||
url.value = '';
|
url.value = ''
|
||||||
return loadContent()
|
return loadContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pImg = e => e.target.naturalHeight > e.target.naturalWidth ? e.target.classList.add('image-portrait') : null
|
const pImg = (e: Event): void => {
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
if (target.naturalHeight > target.naturalWidth) {
|
||||||
|
target.classList.add('image-portrait')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
<style scoped>
|
||||||
|
code {
|
||||||
|
color: var(--bulma-code) !important
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="modal is-active" v-if="false === externalModel">
|
<div class="modal is-active" v-if="false === externalModel">
|
||||||
|
|
@ -31,62 +37,41 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
code {
|
|
||||||
color: var(--bulma-code) !important
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { request } from '~/utils/index'
|
import { request } from '~/utils/index'
|
||||||
|
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
|
const emitter = defineEmits<{ (e: 'closeModel'): void }>()
|
||||||
|
|
||||||
const emitter = defineEmits(['closeModel'])
|
const props = defineProps<{
|
||||||
const isLoading = ref<Boolean>(false)
|
link?: string
|
||||||
|
preset?: string
|
||||||
|
useUrl?: boolean
|
||||||
|
externalModel?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isLoading = ref<boolean>(false)
|
||||||
const data = ref<any>({})
|
const data = ref<any>({})
|
||||||
|
|
||||||
const props = defineProps({
|
const handle_event = (e: KeyboardEvent): void => {
|
||||||
link: {
|
if (e.key === 'Escape') {
|
||||||
type: String,
|
emitter('closeModel')
|
||||||
default: '',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
preset: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
useUrl: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
externalModel: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const handle_event = (e: KeyboardEvent) => {
|
|
||||||
if (e.key !== 'Escape') {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
emitter('closeModel')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async (): Promise<void> => {
|
||||||
document.addEventListener('keydown', handle_event)
|
document.addEventListener('keydown', handle_event)
|
||||||
|
|
||||||
let url = props.useUrl ? props.link : '/api/yt-dlp/url/info';
|
let url = props.useUrl ? props.link || '' : '/api/yt-dlp/url/info'
|
||||||
|
|
||||||
if (!props.useUrl) {
|
if (!props.useUrl) {
|
||||||
let params = new URLSearchParams();
|
const params = new URLSearchParams()
|
||||||
if (props.preset) {
|
if (props.preset) {
|
||||||
params.append('preset', props.preset);
|
params.append('preset', props.preset)
|
||||||
}
|
}
|
||||||
params.append('url', props.link);
|
params.append('url', props.link || '')
|
||||||
url += '?' + params.toString();
|
url += '?' + params.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -96,10 +81,9 @@ onMounted(async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data.value = JSON.parse(body)
|
data.value = JSON.parse(body)
|
||||||
} catch (e) {
|
} catch {
|
||||||
data.value = body
|
data.value = body
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.error(`Error: ${e.message}`)
|
toast.error(`Error: ${e.message}`)
|
||||||
|
|
@ -108,5 +92,7 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('keydown', handle_event)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,77 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="targetEl" :style="`min-height:${fixedMinHeight ? fixedMinHeight : (minHeight)}px`">
|
<div ref="targetEl" :style="`min-height:${fixedMinHeight || props.minHeight || 0}px`">
|
||||||
<slot v-if="shouldRender"/>
|
<slot v-if="shouldRender" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import {nextTick, ref} from 'vue'
|
import { ref, nextTick, onBeforeUnmount } from 'vue'
|
||||||
import {useIntersectionObserver} from '@vueuse/core'
|
import { useIntersectionObserver } from '@vueuse/core'
|
||||||
|
|
||||||
function onIdle(cb = () => {
|
const props = defineProps<{
|
||||||
}) {
|
renderOnIdle?: boolean
|
||||||
if ("requestIdleCallback" in window) {
|
unrender?: boolean
|
||||||
window.requestIdleCallback(cb);
|
minHeight?: number
|
||||||
|
unrenderDelay?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const shouldRender = ref(false)
|
||||||
|
const targetEl = ref<HTMLElement | null>(null)
|
||||||
|
const fixedMinHeight = ref(0)
|
||||||
|
|
||||||
|
let unrenderTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let renderTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
function onIdle(cb: () => void): void {
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
(window as any).requestIdleCallback(cb)
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => nextTick(cb), 300);
|
setTimeout(() => nextTick(cb), 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const { stop } = useIntersectionObserver(targetEl, ([{ isIntersecting }]) => {
|
||||||
props: {
|
if (isIntersecting) {
|
||||||
renderOnIdle: Boolean,
|
if (unrenderTimer) clearTimeout(unrenderTimer)
|
||||||
unrender: Boolean,
|
|
||||||
minHeight: Number,
|
|
||||||
unrenderDelay: {
|
|
||||||
type: Number,
|
|
||||||
default: 6000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const shouldRender = ref(false);
|
|
||||||
const targetEl = ref();
|
|
||||||
const fixedMinHeight = ref(0);
|
|
||||||
let unrenderTimer;
|
|
||||||
let renderTimer;
|
|
||||||
|
|
||||||
const {stop} = useIntersectionObserver(targetEl, ([{isIntersecting}]) => {
|
renderTimer = setTimeout(() => { shouldRender.value = true }, props.unrender ? 200 : 0)
|
||||||
if (isIntersecting) {
|
|
||||||
// perhaps the user re-scrolled to a component that was set to unrender. In that case stop the un-rendering timer
|
|
||||||
clearTimeout(unrenderTimer);
|
|
||||||
// if we're dealing under-rendering lets add a waiting period of 200ms before rendering.
|
|
||||||
// If a component enters the viewport and also leaves it within 200ms it will not render at all.
|
|
||||||
// This saves work and improves performance when user scrolls very fast
|
|
||||||
renderTimer = setTimeout(() => (shouldRender.value = true), props.unrender ? 200 : 0);
|
|
||||||
shouldRender.value = true;
|
|
||||||
if (!props.unrender) {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
} else if (props.unrender) {
|
|
||||||
// if the component was set to render, cancel that
|
|
||||||
clearTimeout(renderTimer);
|
|
||||||
unrenderTimer = setTimeout(() => {
|
|
||||||
if (targetEl.value?.clientHeight) {
|
|
||||||
fixedMinHeight.value = targetEl.value.clientHeight;
|
|
||||||
}
|
|
||||||
shouldRender.value = false;
|
|
||||||
}, props.unrenderDelay);
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
rootMargin: "600px",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (props.renderOnIdle) {
|
shouldRender.value = true
|
||||||
onIdle(() => {
|
|
||||||
shouldRender.value = true;
|
if (!props.unrender) {
|
||||||
if (!props.unrender) {
|
stop()
|
||||||
stop();
|
}
|
||||||
}
|
}
|
||||||
});
|
else if (props.unrender) {
|
||||||
|
if (renderTimer) {
|
||||||
|
clearTimeout(renderTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {targetEl, shouldRender, fixedMinHeight};
|
unrenderTimer = setTimeout(() => {
|
||||||
},
|
if (targetEl.value?.clientHeight) {
|
||||||
};
|
fixedMinHeight.value = targetEl.value.clientHeight
|
||||||
|
}
|
||||||
|
shouldRender.value = false
|
||||||
|
}, props.unrenderDelay ?? 6000)
|
||||||
|
}
|
||||||
|
}, { rootMargin: '600px', })
|
||||||
|
|
||||||
|
if (props.renderOnIdle) {
|
||||||
|
onIdle(() => {
|
||||||
|
shouldRender.value = true
|
||||||
|
if (!props.unrender) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (renderTimer) {
|
||||||
|
clearTimeout(renderTimer)
|
||||||
|
|
||||||
|
}
|
||||||
|
if (unrenderTimer) {
|
||||||
|
clearTimeout(unrenderTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="notification" :class="message_class">
|
<div class="notification" :class="props.message_class">
|
||||||
<button class="delete" @click="$emit('close')" v-if="!useToggle && useClose"></button>
|
<button class="delete" @click="emit('close')" v-if="!props.useToggle && props.useClose"></button>
|
||||||
<div @click="$emit('toggle')" class="is-clickable is-pulled-right is-unselectable" v-if="useToggle">
|
|
||||||
|
<div @click="emit('toggle')" class="is-clickable is-pulled-right is-unselectable" v-if="props.useToggle">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas" :class="{ 'fa-arrow-up': toggle, 'fa-arrow-down': !toggle }"></i>
|
<i class="fas" :class="{ 'fa-arrow-up': props.toggle, 'fa-arrow-down': !props.toggle }"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{{ toggle ? 'Close' : 'Open' }}</span>
|
<span>{{ props.toggle ? 'Close' : 'Open' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification-title is-unselectable" :class="{ 'is-clickable': useToggle }" v-if="title || icon"
|
|
||||||
@click="useToggle ? $emit('toggle', toggle) : null">
|
<div class="notification-title is-unselectable" :class="{ 'is-clickable': props.useToggle }"
|
||||||
<template v-if="icon">
|
v-if="props.title || props.icon" @click="props.useToggle ? emit('toggle', props.toggle) : null">
|
||||||
|
<template v-if="props.icon">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon"><i :class="icon"></i></span>
|
<span class="icon"><i :class="props.icon"></i></span>
|
||||||
<span>{{ title }}</span>
|
<span>{{ props.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>{{ title }}</template>
|
<template v-else>{{ props.title }}</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification-content content" v-if="false === useToggle || toggle">
|
|
||||||
<template v-if="message">{{ message }}</template>
|
<div class="notification-content content" v-if="!props.useToggle || props.toggle">
|
||||||
|
<template v-if="props.message">{{ props.message }}</template>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
defineProps({
|
const props = defineProps < {
|
||||||
title: {
|
title?: string
|
||||||
type: String,
|
icon?: string
|
||||||
default: null,
|
message?: string
|
||||||
required: false
|
message_class?: string
|
||||||
},
|
useToggle?: boolean
|
||||||
icon: {
|
toggle?: boolean
|
||||||
type: String,
|
useClose?: boolean
|
||||||
default: null,
|
} > ()
|
||||||
required: false
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
message_class: {
|
|
||||||
type: String,
|
|
||||||
default: 'is-info',
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
useToggle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
toggle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
useClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
defineEmits(['toggle', 'close'])
|
const emit = defineEmits < {
|
||||||
|
(e: 'toggle', value ?: boolean): void
|
||||||
|
(e: 'close'): void
|
||||||
|
}> ()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -7,33 +7,31 @@
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<label class="label is-inline is-unselectable" for="url">
|
<label class="label is-inline is-unselectable" for="url">
|
||||||
<span class="icon"><i class="fa-solid fa-link" /></span>
|
<span class="icon"><i class="fa-solid fa-link" /></span>
|
||||||
URLs
|
URLs separated by <span class="is-bold">{{ getSeparatorsName(separator) }}</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<input type="text" class="input" id="url" placeholder="Video or playlist link"
|
<input type="text" class="input" id="url" placeholder="URLs to download"
|
||||||
:disabled="!socket.isConnected || addInProgress" v-model="form.url">
|
:disabled="!socket.isConnected || addInProgress" v-model="form.url">
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-align-content-space-around" v-if="!config.app.basic_mode"
|
<div class="control">
|
||||||
v-tooltip="'Auto start the download after adding it.'">
|
<button type="submit" class="button is-primary"
|
||||||
<input id="auto_start" type="checkbox" v-model="auto_start" :disabled="addInProgress"
|
:class="{ 'is-loading': !socket.isConnected || addInProgress }"
|
||||||
class="switch is-success" />
|
:disabled="!socket.isConnected || addInProgress || !form?.url">
|
||||||
<label for="auto_start" class="is-unselectable">Start</label>
|
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||||
|
<span>Add</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="help is-bold is-unselectable">
|
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
|
||||||
<span>You can add multiple URLs separated by <code
|
|
||||||
class="is-bold">{{ getSeparatorsName(separator) }}</code>.</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode">
|
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control" @click="show_description = !show_description">
|
||||||
<a href="#" class="button is-static">
|
<label class="button is-static">
|
||||||
<span class="icon"><i class="fa-solid fa-sliders" /></span>
|
<span class="icon"><i class="fa-solid fa-sliders" /></span>
|
||||||
<span>Preset</span>
|
<span>Preset</span>
|
||||||
</a>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
|
@ -55,13 +53,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile" v-if="!config.app.basic_mode">
|
<div class="column is-6-tablet is-12-mobile" v-if="!config.app.basic_mode">
|
||||||
<div class="field has-addons" v-tooltip="'Folder relative to ' + config.app.download_path">
|
<div class="field has-addons" v-tooltip="'Folder relative to ' + config.app.download_path">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a href="#" class="button is-static">
|
<label class="button is-static">
|
||||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||||
<span>Save in</span>
|
<span>Save in</span>
|
||||||
</a>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<input type="text" class="input is-fullwidth" id="path" v-model="form.folder" placeholder="Default"
|
<input type="text" class="input is-fullwidth" id="path" v-model="form.folder" placeholder="Default"
|
||||||
|
|
@ -69,23 +68,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
|
||||||
<button type="submit" class="button is-primary"
|
|
||||||
:class="{ 'is-loading': !socket.isConnected || addInProgress }"
|
|
||||||
:disabled="!socket.isConnected || addInProgress || !form?.url">
|
|
||||||
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
|
||||||
<span>Add</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="column" v-if="!config.app.basic_mode">
|
<div class="column" v-if="!config.app.basic_mode">
|
||||||
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
|
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
|
||||||
:class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected">
|
:class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected">
|
||||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||||
<span>Opts</span>
|
<span>Advanced Options</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-12"
|
<div class="column is-12"
|
||||||
v-if="!config.app.basic_mode && !hasFormatInConfig && get_preset(form.preset)?.description">
|
v-if="show_description && !config.app.basic_mode && !hasFormatInConfig && get_preset(form.preset)?.description">
|
||||||
<div class="is-overflow-auto" style="max-height: 150px;">
|
<div class="is-overflow-auto" style="max-height: 150px;">
|
||||||
<div class="is-ellipsis is-clickable" @click="expand_description">
|
<div class="is-ellipsis is-clickable" @click="expand_description">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span> {{ get_preset(form.preset)?.description }}
|
<span class="icon"><i class="fa-solid fa-info" /></span> {{ get_preset(form.preset)?.description }}
|
||||||
|
|
@ -97,44 +90,36 @@
|
||||||
<div class="columns is-multiline is-mobile" v-if="showAdvanced && !config.app.basic_mode">
|
<div class="columns is-multiline is-mobile" v-if="showAdvanced && !config.app.basic_mode">
|
||||||
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode">
|
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline is-unselectable">
|
<input id="auto_start" type="checkbox" v-model="auto_start" :disabled="addInProgress"
|
||||||
<span class="icon"><i class="fa-solid fa-object-ungroup" /></span>
|
class="switch is-success" />
|
||||||
<span>URLs Separator</span>
|
<label for="auto_start" class="is-unselectable">
|
||||||
|
{{ auto_start ? 'Auto start' : 'Manual start' }}
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
|
||||||
<div class="select is-fullwidth">
|
|
||||||
<select class="is-fullwidth" :disabled="!socket.isConnected || addInProgress" v-model="separator">
|
|
||||||
<option v-for="(sep, index) in separators" :key="`sep-${index}`" :value="sep.value">
|
|
||||||
{{ sep.name }} ({{ sep.value }})
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="help is-bold">
|
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
|
||||||
<span>Use this to separate multiple URLs in the input field.</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span class="is-bold">Whether to start the download automatically or wait.</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-8-tablet is-12-mobile">
|
<div class="column is-8-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field has-addons">
|
||||||
<label class="label is-inline is-unselectable" for="output_format"
|
|
||||||
v-tooltip="'Default: ' + config.app.output_template">
|
|
||||||
<span class="icon"><i class="fa-solid fa-file" /></span>
|
|
||||||
Output Template
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="text" class="input" v-model="form.template" id="output_format"
|
<label for="output_format" class="button is-static is-unselectable">
|
||||||
:disabled="!socket.isConnected || addInProgress"
|
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||||
placeholder="Uses default output template naming if empty.">
|
<span>Template</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input type="text" class="input" v-model="form.template" id="output_format"
|
||||||
|
:disabled="!socket.isConnected || addInProgress" :placeholder="get_output_template()">
|
||||||
</div>
|
</div>
|
||||||
<span class="help is-bold">
|
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
|
||||||
<span>All output template naming options can be found at <NuxtLink target="_blank"
|
|
||||||
to="https://github.com/yt-dlp/yt-dlp#output-template">this page</NuxtLink>.</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="help is-bold is-unselectable">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>All output template naming options can be found at <NuxtLink target="_blank"
|
||||||
|
to="https://github.com/yt-dlp/yt-dlp#output-template">this page</NuxtLink>.</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
|
@ -154,7 +139,7 @@
|
||||||
<span>Check <NuxtLink target="_blank" to="https://github.com/yt-dlp/yt-dlp#general-options">this page
|
<span>Check <NuxtLink target="_blank" to="https://github.com/yt-dlp/yt-dlp#general-options">this page
|
||||||
</NuxtLink> for more info. <span class="has-text-danger">Not all options are supported <NuxtLink
|
</NuxtLink> for more info. <span class="has-text-danger">Not all options are supported <NuxtLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L24-L46">some are
|
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26-L48">some are
|
||||||
ignored</NuxtLink>. Use with caution these options can break yt-dlp or the frontend.</span>
|
ignored</NuxtLink>. Use with caution these options can break yt-dlp or the frontend.</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -222,7 +207,7 @@
|
||||||
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="button" class="button is-danger" @click="resetConfig"
|
<button type="button" class="button is-danger" @click="resetConfig"
|
||||||
:disabled="!socket.isConnected || form?.id" v-tooltip="'Reset local settings'">
|
:disabled="!!(!socket.isConnected || form?.id)" v-tooltip="'Reset local settings'">
|
||||||
<span class="icon"><i class="fa-solid fa-rotate-left" /></span>
|
<span class="icon"><i class="fa-solid fa-rotate-left" /></span>
|
||||||
<span>Reset</span>
|
<span>Reset</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -237,47 +222,36 @@
|
||||||
<datalist id="folders" v-if="config?.folders">
|
<datalist id="folders" v-if="config?.folders">
|
||||||
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
||||||
</datalist>
|
</datalist>
|
||||||
|
<ConfirmDialog v-if="dialog_confirm.visible" :visible="dialog_confirm.visible" :title="dialog_confirm.title"
|
||||||
|
:message="dialog_confirm.message" :options="dialog_confirm.options" @confirm="dialog_confirm.confirm"
|
||||||
|
@cancel="() => dialog_confirm.visible = false" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import 'assets/css/bulma-switch.css'
|
import 'assets/css/bulma-switch.css'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import type { item_request } from '~/@types/item'
|
||||||
|
import { getSeparatorsName, separators } from '~/utils/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps<{ item?: Partial<item_request> }>()
|
||||||
item: {
|
const emitter = defineEmits<{
|
||||||
type: Object,
|
(e: 'getInfo', url: string, preset: string | undefined): void
|
||||||
required: false,
|
(e: 'clear_form'): void
|
||||||
default: () => { },
|
(e: 'remove_archive', url: string): void
|
||||||
},
|
}>()
|
||||||
})
|
|
||||||
|
|
||||||
const emitter = defineEmits(['getInfo', 'clear_form', 'remove_archive'])
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
const box = useConfirm()
|
|
||||||
|
|
||||||
const separators = [
|
const showAdvanced = useStorage<boolean>('show_advanced', false)
|
||||||
{ name: 'Comma', value: ',', },
|
const separator = useStorage<string>('url_separator', separators[0].value)
|
||||||
{ name: 'Semicolon', value: ';', },
|
const auto_start = useStorage<boolean>('auto_start', true)
|
||||||
{ name: 'Colon', value: ':', },
|
const show_description = useStorage<boolean>('show_description', true)
|
||||||
{ name: 'Pipe', value: '|', },
|
|
||||||
{ name: 'Space', value: ' ', }
|
|
||||||
]
|
|
||||||
|
|
||||||
const getSeparatorsName = (value) => {
|
const addInProgress = ref<boolean>(false)
|
||||||
const sep = separators.find(s => s.value === value)
|
|
||||||
return sep ? `${sep.name} (${value})` : 'Unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
const showAdvanced = useStorage('show_advanced', false)
|
const form = useStorage<item_request>('local_config_v1', {
|
||||||
const separator = useStorage('url_separator', separators[0].value)
|
|
||||||
const auto_start = useStorage('auto_start', true)
|
|
||||||
|
|
||||||
const addInProgress = ref(false)
|
|
||||||
|
|
||||||
const form = useStorage('local_config_v1', {
|
|
||||||
id: null,
|
id: null,
|
||||||
url: '',
|
url: '',
|
||||||
preset: config.app.default_preset,
|
preset: config.app.default_preset,
|
||||||
|
|
@ -286,6 +260,14 @@ const form = useStorage('local_config_v1', {
|
||||||
template: '',
|
template: '',
|
||||||
folder: '',
|
folder: '',
|
||||||
extras: {},
|
extras: {},
|
||||||
|
}) as Ref<item_request>
|
||||||
|
|
||||||
|
const dialog_confirm = ref({
|
||||||
|
visible: false,
|
||||||
|
title: 'Confirm Action',
|
||||||
|
confirm: () => { },
|
||||||
|
message: '',
|
||||||
|
options: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const addDownload = async () => {
|
const addDownload = async () => {
|
||||||
|
|
@ -296,9 +278,9 @@ const addDownload = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const request_data = []
|
const request_data = [] as Array<item_request>
|
||||||
|
|
||||||
form.value.url.split(separator.value).forEach(async (url) => {
|
form.value.url.split(separator.value).forEach(async (url: string) => {
|
||||||
if (!url.trim()) {
|
if (!url.trim()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -311,7 +293,7 @@ const addDownload = async () => {
|
||||||
cookies: config.app.basic_mode ? '' : form.value.cookies,
|
cookies: config.app.basic_mode ? '' : form.value.cookies,
|
||||||
cli: config.app.basic_mode ? null : form.value.cli,
|
cli: config.app.basic_mode ? null : form.value.cli,
|
||||||
auto_start: config.app.basic_mode ? true : auto_start.value
|
auto_start: config.app.basic_mode ? true : auto_start.value
|
||||||
}
|
} as item_request
|
||||||
|
|
||||||
if (form.value?.extras && Object.keys(form.value.extras).length > 0) {
|
if (form.value?.extras && Object.keys(form.value.extras).length > 0) {
|
||||||
data.extras = form.value.extras
|
data.extras = form.value.extras
|
||||||
|
|
@ -334,7 +316,7 @@ const addDownload = async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach((item: Record<string, any>) => {
|
||||||
if (false !== item.status) {
|
if (false !== item.status) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +326,7 @@ const addDownload = async () => {
|
||||||
form.value.url = ''
|
form.value.url = ''
|
||||||
emitter('clear_form')
|
emitter('clear_form')
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.error(`Error: ${e.message}`)
|
toast.error(`Error: ${e.message}`)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -353,12 +335,13 @@ const addDownload = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetConfig = () => {
|
const resetConfig = () => {
|
||||||
if (true !== box.confirm('Reset your local configuration?')) {
|
dialog_confirm.value.visible = true
|
||||||
return
|
dialog_confirm.value.message = `Reset local configuration?`
|
||||||
}
|
dialog_confirm.value.confirm = reset_config
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset_config = () => {
|
||||||
form.value = {
|
form.value = {
|
||||||
id: null,
|
|
||||||
url: '',
|
url: '',
|
||||||
preset: config.app.default_preset,
|
preset: config.app.default_preset,
|
||||||
cookies: '',
|
cookies: '',
|
||||||
|
|
@ -366,15 +349,15 @@ const resetConfig = () => {
|
||||||
template: '',
|
template: '',
|
||||||
folder: '',
|
folder: '',
|
||||||
extras: {},
|
extras: {},
|
||||||
}
|
} as item_request
|
||||||
|
|
||||||
showAdvanced.value = false
|
showAdvanced.value = false
|
||||||
separator.value = separators[0].value
|
|
||||||
|
|
||||||
toast.success('Local configuration has been reset.')
|
toast.success('Local configuration has been reset.')
|
||||||
|
dialog_confirm.value.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertOptions = async args => {
|
const convertOptions = async (args: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await convertCliOptions(args)
|
const response = await convertCliOptions(args)
|
||||||
|
|
||||||
|
|
@ -387,7 +370,7 @@ const convertOptions = async args => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.opts
|
return response.opts
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,36 +401,30 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props?.item) {
|
if (props?.item) {
|
||||||
Object.keys(props.item).forEach(key => {
|
const updates: Partial<item_request> = {}
|
||||||
if (key in form.value) {
|
const keys = Object.keys(props.item) as (keyof item_request)[]
|
||||||
let value = props.item[key]
|
for (const key of keys) {
|
||||||
if ('extras' === key) {
|
const value = props.item[key]
|
||||||
value = JSON.parse(JSON.stringify(props.item[key]))
|
updates[key] = key === 'extras' ? JSON.parse(JSON.stringify(value)) : value!
|
||||||
}
|
}
|
||||||
form.value[key] = value
|
form.value = { ...form.value, ...updates }
|
||||||
}
|
emitter('clear_form')
|
||||||
})
|
|
||||||
emitter('clear_form');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
if (!separators.some(s => s.value === separator.value)) {
|
if (!separators.some(s => s.value === separator.value)) {
|
||||||
separator.value = separators[0].value
|
separator.value = separators[0].value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasFormatInConfig = computed(() => {
|
const hasFormatInConfig = computed((): boolean => !!form.value.cli?.match(/(?<!\S)(-f|--format)(=|\s)(\S+)/))
|
||||||
if (!form.value?.cli) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return /(?<!\S)(-f|--format)(=|\s)(\S+)/.test(form.value.cli)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
|
const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
|
||||||
const get_preset = name => config.presets.find(item => item.name === name)
|
const get_preset = (name: string | undefined) => config.presets.find(item => item.name === name)
|
||||||
const expand_description = e => toggleClass(e.target, ['is-ellipsis', 'is-pre-wrap'])
|
const expand_description = (e: Event) => toggleClass(e.target, ['is-ellipsis', 'is-pre-wrap'])
|
||||||
|
|
||||||
const removeFromArchive = async url => {
|
const removeFromArchive = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
const req = await request(`/api/archive/0`, {
|
const req = await request(`/api/archive/0`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
@ -463,8 +440,19 @@ const removeFromArchive = async url => {
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(data.message ?? `Removed item from archive.`)
|
toast.success(data.message ?? `Removed item from archive.`)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
toast.error(`Error: ${e.message}`)
|
toast.error(`Error: ${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const get_output_template = () => {
|
||||||
|
if (form.value.preset && !hasFormatInConfig.value) {
|
||||||
|
const preset = config.presets.find(p => p.name === form.value.preset)
|
||||||
|
if (preset && preset.template) {
|
||||||
|
return preset.template
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config.app.output_template || '%(title)s.%(ext)s'
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,7 @@ const importItem = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.target) {
|
if (form.target) {
|
||||||
if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) {
|
if (false === box.confirm('Overwrite the current form fields?', true)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -523,7 +523,7 @@ const updateProgress = (item) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmCancel = item => {
|
const confirmCancel = item => {
|
||||||
if (true !== box.confirm(`Are you sure you want to cancel (${item.title})?`)) {
|
if (true !== box.confirm(`Cancel '${item.title}'?`)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
cancelItems(item._id)
|
cancelItems(item._id)
|
||||||
|
|
@ -531,7 +531,7 @@ const confirmCancel = item => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelSelected = () => {
|
const cancelSelected = () => {
|
||||||
if (true !== box.confirm('Are you sure you want to cancel selected items?')) {
|
if (true !== box.confirm(`Cancel '${selectedElms.value.length}' selected items?`)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
cancelItems(selectedElms.value)
|
cancelItems(selectedElms.value)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-unselectable" for="random_bg_opacity">
|
<label class="label is-unselectable" for="random_bg_opacity">
|
||||||
Background visibility <code>{{ parseFloat(1.0 - bg_opacity).toFixed(2) }}</code>
|
Background visibility <code>{{ parseFloat(String(1.0 - bg_opacity)).toFixed(2) }}</code>
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50"
|
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50"
|
||||||
|
|
@ -69,6 +69,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-unselectable" for="show_thumbnail">URLs Separator</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select class="is-fullwidth" v-model="separator">
|
||||||
|
<option v-for="(sep, index) in separators" :key="`sep-${index}`" :value="sep.value">
|
||||||
|
{{ sep.name }} ({{ sep.value }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
@ -126,24 +139,20 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { POSITION } from 'vue-toastification'
|
import { POSITION } from 'vue-toastification'
|
||||||
|
import { separators } from '~/utils/utils'
|
||||||
|
|
||||||
defineProps({
|
defineProps<{ isLoading: boolean }>()
|
||||||
isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const bg_enable = useStorage('random_bg', true)
|
|
||||||
const bg_opacity = useStorage('random_bg_opacity', 0.95)
|
|
||||||
const selectedTheme = useStorage('theme', 'auto')
|
|
||||||
const allow_toasts = useStorage('allow_toasts', true)
|
|
||||||
const reduce_confirm = useStorage('reduce_confirm', false)
|
|
||||||
const toast_position = useStorage('toast_position', POSITION.TOP_RIGHT)
|
|
||||||
const toast_dismiss_on_click = useStorage('toast_dismiss_on_click', true)
|
|
||||||
const show_thumbnail = useStorage('show_thumbnail', true)
|
|
||||||
|
|
||||||
|
const bg_enable = useStorage<boolean>('random_bg', true)
|
||||||
|
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
|
||||||
|
const selectedTheme = useStorage<'auto' | 'light' | 'dark'>('theme', 'auto')
|
||||||
|
const allow_toasts = useStorage<boolean>('allow_toasts', true)
|
||||||
|
const reduce_confirm = useStorage<boolean>('reduce_confirm', false)
|
||||||
|
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
|
||||||
|
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
|
||||||
|
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
||||||
|
const separator = useStorage<string>('url_separator', separators[0].value)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,9 @@ onMounted(() => {
|
||||||
if (!props.task?.preset || '' === props.task.preset) {
|
if (!props.task?.preset || '' === props.task.preset) {
|
||||||
form.preset = toRaw(config.app.default_preset)
|
form.preset = toRaw(config.app.default_preset)
|
||||||
}
|
}
|
||||||
|
if (typeof form.auto_start === 'undefined' || form.auto_start === null) {
|
||||||
|
form.auto_start = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkInfo = async () => {
|
const checkInfo = async () => {
|
||||||
|
|
@ -384,7 +387,7 @@ const importItem = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.url || form.timer) {
|
if (form.url || form.timer) {
|
||||||
if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) {
|
if (false === box.confirm('Overwrite the current form fields?', true)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-info" @click="reloadContent" :class="{ 'is-loading': isLoading }"
|
<button class="button is-info" @click="reloadContent(false)" :class="{ 'is-loading': isLoading }"
|
||||||
:disabled="!socket.isConnected || isLoading" v-if="items && items.length > 0">
|
:disabled="!socket.isConnected || isLoading" v-if="items && items.length > 0">
|
||||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -30,8 +30,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-12" v-if="toggleForm">
|
<div class="column is-12" v-if="toggleForm">
|
||||||
<ConditionForm :addInProgress="addInProgress" :reference="itemRef" :item="item" @cancel="resetForm(true)"
|
<ConditionForm :addInProgress="addInProgress" :reference="itemRef" :item="item as ConditionItem"
|
||||||
@submit="updateItem" />
|
@cancel="resetForm(true)" @submit="updateItem" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-12" v-if="!toggleForm">
|
<div class="column is-12" v-if="!toggleForm">
|
||||||
|
|
@ -114,18 +114,19 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { request } from '~/utils/index'
|
import { request } from '~/utils/index'
|
||||||
import { encode } from '~/utils/importer'
|
import { encode } from '~/utils/importer'
|
||||||
|
import type { ConditionItem, ImportedConditionItem } from '~/@types/conditions'
|
||||||
|
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
const box = useConfirm()
|
const box = useConfirm()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref<ConditionItem[]>([])
|
||||||
const item = ref({})
|
const item = ref<Partial<ConditionItem>>({})
|
||||||
const itemRef = ref("")
|
const itemRef = ref<string | null | undefined>("")
|
||||||
const toggleForm = ref(false)
|
const toggleForm = ref(false)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const initialLoad = ref(true)
|
const initialLoad = ref(true)
|
||||||
|
|
@ -146,15 +147,15 @@ watch(() => socket.isConnected, async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const reloadContent = async (fromMounted = false) => {
|
|
||||||
|
const reloadContent = async (fromMounted = false): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
const response = await request("/api/conditions")
|
const response = await request('/api/conditions')
|
||||||
|
|
||||||
if (fromMounted && !response.ok) {
|
if (fromMounted && !response.ok) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (data.length < 1) {
|
if (data.length < 1) {
|
||||||
return
|
return
|
||||||
|
|
@ -162,17 +163,16 @@ const reloadContent = async (fromMounted = false) => {
|
||||||
|
|
||||||
items.value = data
|
items.value = data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (fromMounted) {
|
if (!fromMounted) {
|
||||||
return
|
console.error(e)
|
||||||
|
toast.error('Failed to fetch page content.')
|
||||||
}
|
}
|
||||||
console.error(e)
|
|
||||||
toast.error("Failed to fetch page content.")
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetForm = (closeForm = false) => {
|
const resetForm = (closeForm = false): void => {
|
||||||
item.value = {}
|
item.value = {}
|
||||||
itemRef.value = null
|
itemRef.value = null
|
||||||
addInProgress.value = false
|
addInProgress.value = false
|
||||||
|
|
@ -181,30 +181,29 @@ const resetForm = (closeForm = false) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateItems = async items => {
|
const updateItems = async (newItems: ConditionItem[]): Promise<boolean> => {
|
||||||
let data
|
let data: any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addInProgress.value = true
|
addInProgress.value = true
|
||||||
|
|
||||||
const local_items = []
|
const validItems = newItems.map(({ id, name, filter, cli }) => {
|
||||||
for (const item of items) {
|
|
||||||
const { id, name, filter, cli } = item
|
|
||||||
if (!name || !filter || !cli) {
|
if (!name || !filter || !cli) {
|
||||||
toast.error("Name, filter and cli are required.")
|
toast.error('Name, filter and cli are required.')
|
||||||
return false
|
throw new Error('Missing fields')
|
||||||
}
|
}
|
||||||
local_items.push({ id, name, filter, cli })
|
return { id, name, filter, cli }
|
||||||
}
|
|
||||||
|
|
||||||
const response = await request("/api/conditions", {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(local_items),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
data = await response.json()
|
const response = await request('/api/conditions', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(validItems),
|
||||||
|
})
|
||||||
|
|
||||||
if (200 !== response.status) {
|
data = await response.json() as ConditionItem[]
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
toast.error(`Failed to update items. ${data.error}`)
|
toast.error(`Failed to update items. ${data.error}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -212,44 +211,46 @@ const updateItems = async items => {
|
||||||
items.value = data
|
items.value = data
|
||||||
resetForm(true)
|
resetForm(true)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
toast.error(`Failed to update items. ${data?.error}. ${e.message}`)
|
toast.error(`Failed to update items. ${data?.error ?? ''} ${e.message}`)
|
||||||
} finally {
|
} finally {
|
||||||
addInProgress.value = false
|
addInProgress.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteItem = async (item) => {
|
const deleteItem = async (cond: ConditionItem): Promise<void> => {
|
||||||
if (true !== box.confirm(`Delete '${item.name}'?`, true)) {
|
if (!box.confirm(`Delete '${cond.name}'?`, true)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = items.value.findIndex((t) => t?.id === item.id)
|
const index = items.value.findIndex(t => t?.id === cond.id)
|
||||||
if (index > -1) {
|
if (-1 === index) {
|
||||||
items.value.splice(index, 1)
|
toast.error('Item not found.')
|
||||||
} else {
|
|
||||||
toast.error("Item not found.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.value.splice(index, 1)
|
||||||
const status = await updateItems(items.value)
|
const status = await updateItems(items.value)
|
||||||
|
if (status) {
|
||||||
if (!status) {
|
toast.success('Item deleted.')
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Item deleted.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateItem = async ({ reference, item }) => {
|
const updateItem = async ({ reference, item: updatedItem, }: {
|
||||||
item = cleanObject(item, remove_keys)
|
reference: string | null | undefined,
|
||||||
|
item: ConditionItem
|
||||||
|
}): Promise<void> => {
|
||||||
|
updatedItem = cleanObject(updatedItem, remove_keys)
|
||||||
|
|
||||||
if (reference) {
|
if (reference) {
|
||||||
const index = items.value.findIndex(t => t?.id === reference)
|
const index = items.value.findIndex(t => t?.id === reference)
|
||||||
if (index > -1) {
|
if (index !== -1) {
|
||||||
items.value[index] = item
|
items.value[index] = updatedItem
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items.value.push(item)
|
items.value.push(updatedItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await updateItems(items.value)
|
const status = await updateItems(items.value)
|
||||||
|
|
@ -257,36 +258,32 @@ const updateItem = async ({ reference, item }) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`Item ${reference ? "updated" : "added"}.`)
|
toast.success(`Item ${reference ? 'updated' : 'added'}.`)
|
||||||
resetForm(true)
|
resetForm(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editItem = _item => {
|
const editItem = (_item: ConditionItem): void => {
|
||||||
item.value = _item
|
item.value = { ..._item }
|
||||||
itemRef.value = _item.id
|
itemRef.value = _item.id
|
||||||
toggleForm.value = true
|
toggleForm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => (socket.isConnected ? await reloadContent(true) : ""))
|
const exportItem = (cond: ConditionItem): void => {
|
||||||
|
const clone: Partial<ImportedConditionItem> = JSON.parse(JSON.stringify(cond))
|
||||||
|
delete clone.id
|
||||||
|
|
||||||
const exportItem = item => {
|
const userData: ImportedConditionItem = {
|
||||||
let data = JSON.parse(JSON.stringify(item))
|
...Object.fromEntries(Object.entries(clone).filter(([_, v]) => !!v)),
|
||||||
if ("id" in data) {
|
_type: 'condition',
|
||||||
delete data['id']
|
_version: '1.0',
|
||||||
}
|
} as ImportedConditionItem
|
||||||
|
|
||||||
let userData = {}
|
copyText(encode(userData))
|
||||||
|
|
||||||
for (const key of Object.keys(data)) {
|
|
||||||
if (!data[key]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
userData[key] = data[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
userData['_type'] = 'condition'
|
|
||||||
userData['_version'] = '1.0'
|
|
||||||
|
|
||||||
return copyText(encode(userData))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (socket.isConnected) {
|
||||||
|
await reloadContent(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -18,27 +18,28 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-danger is-light" v-tooltip.bottom="'Filter'"
|
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
|
||||||
@click="toggleFilter = !toggleFilter">
|
|
||||||
<span class="icon"><i class="fas fa-filter" /></span>
|
<span class="icon"><i class="fas fa-filter" /></span>
|
||||||
|
<span class="is-hidden-mobile">Filter</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode">
|
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode">
|
||||||
<button class="button is-warning" @click="pauseDownload" v-if="false === config.paused"
|
<button class="button is-warning" @click="pauseDownload" v-if="false === config.paused">
|
||||||
v-tooltip.bottom="'Pause non-active downloads.'">
|
|
||||||
<span class="icon"><i class="fas fa-pause" /></span>
|
<span class="icon"><i class="fas fa-pause" /></span>
|
||||||
|
<span class="is-hidden-mobile">Pause</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-danger" @click="socket.emit('resume', {})" v-else
|
<button class="button is-danger" @click="socket.emit('resume', {})" v-else
|
||||||
v-tooltip.bottom="'Resume downloading.'">
|
v-tooltip.bottom="'Resume downloading.'">
|
||||||
<span class="icon"><i class="fas fa-play" /></span>
|
<span class="icon"><i class="fas fa-play" /></span>
|
||||||
|
<span class="is-hidden-mobile">Resume</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode">
|
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode">
|
||||||
<button v-tooltip.bottom="'Toggle download form'" class="button is-primary has-tooltip-bottom"
|
<button class="button is-primary has-tooltip-bottom" @click="config.showForm = !config.showForm">
|
||||||
@click="config.showForm = !config.showForm">
|
|
||||||
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||||
|
<span class="is-hidden-mobile">New Download</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
@click="() => changeDisplay()">
|
@click="() => changeDisplay()">
|
||||||
<span class="icon"><i class="fa-solid"
|
<span class="icon"><i class="fa-solid"
|
||||||
:class="{ 'fa-table': display_style === 'cards', 'fa-table-list': display_style === 'list' }" /></span>
|
:class="{ 'fa-table': display_style === 'cards', 'fa-table-list': display_style === 'list' }" /></span>
|
||||||
|
<span class="is-hidden-mobile">{{ display_style === 'cards' ? 'Cards' : 'List' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -66,15 +68,19 @@
|
||||||
<Queue @getInfo="(url: string, preset: string = '') => view_info(url, false, preset)" :thumbnails="show_thumbnail"
|
<Queue @getInfo="(url: string, preset: string = '') => view_info(url, false, preset)" :thumbnails="show_thumbnail"
|
||||||
:query="query" @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
|
:query="query" @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
|
||||||
<History @getInfo="(url: string, preset: string = '') => view_info(url, false, preset)"
|
<History @getInfo="(url: string, preset: string = '') => view_info(url, false, preset)"
|
||||||
@add_new="item => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail"
|
@add_new="(item: item_request) => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail"
|
||||||
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
|
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
|
||||||
<GetInfo v-if="info_view.url" :link="info_view.url" :preset="info_view.preset" :useUrl="info_view.useUrl"
|
<GetInfo v-if="info_view.url" :link="info_view.url" :preset="info_view.preset" :useUrl="info_view.useUrl"
|
||||||
@closeModel="close_info()" />
|
@closeModel="close_info()" />
|
||||||
|
<ConfirmDialog v-if="dialog_confirm.visible" :visible="dialog_confirm.visible" :title="dialog_confirm.title"
|
||||||
|
:html_message="dialog_confirm.html_message" :options="dialog_confirm.options" @confirm="dialog_confirm.confirm"
|
||||||
|
@cancel="() => dialog_confirm.visible = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import type { item_request } from '~/@types/item'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const stateStore = useStateStore()
|
const stateStore = useStateStore()
|
||||||
|
|
@ -90,9 +96,16 @@ const info_view = ref({
|
||||||
preset: '',
|
preset: '',
|
||||||
useUrl: false,
|
useUrl: false,
|
||||||
}) as Ref<{ url: string, preset: string, useUrl: boolean }>
|
}) as Ref<{ url: string, preset: string, useUrl: boolean }>
|
||||||
const item_form = ref({})
|
const item_form = ref<item_request | object>({})
|
||||||
const query = ref()
|
const query = ref()
|
||||||
const toggleFilter = ref(false)
|
const toggleFilter = ref(false)
|
||||||
|
const dialog_confirm = ref({
|
||||||
|
visible: false,
|
||||||
|
title: 'Confirm Action',
|
||||||
|
confirm: () => { },
|
||||||
|
html_message: '',
|
||||||
|
options: [],
|
||||||
|
})
|
||||||
|
|
||||||
watch(toggleFilter, () => {
|
watch(toggleFilter, () => {
|
||||||
if (!toggleFilter.value) {
|
if (!toggleFilter.value) {
|
||||||
|
|
@ -122,12 +135,18 @@ watch(() => stateStore.queue, () => {
|
||||||
useHead({ title: `YTPTube: ( ${Object.keys(stateStore.queue).length || 0}/${config.app.max_workers} | ${Object.keys(stateStore.history).length || 0} )` })
|
useHead({ title: `YTPTube: ( ${Object.keys(stateStore.queue).length || 0}/${config.app.max_workers} | ${Object.keys(stateStore.history).length || 0} )` })
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
const pauseDownload = () => {
|
|
||||||
if (false === box.confirm('Are you sure you want to pause all non-active downloads?')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit('pause', {})
|
const pauseDownload = () => {
|
||||||
|
dialog_confirm.value.visible = true
|
||||||
|
dialog_confirm.value.html_message = `<span class="is-bold">Pause All non-active downloads?</span><br>
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa-solid fa-exclamation-triangle"></i></span>
|
||||||
|
<span>This will not stop downloads that are currently in progress.</span>
|
||||||
|
</span>`
|
||||||
|
dialog_confirm.value.confirm = () => {
|
||||||
|
socket.emit('pause', {})
|
||||||
|
dialog_confirm.value.visible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const close_info = () => {
|
const close_info = () => {
|
||||||
|
|
@ -152,7 +171,7 @@ watch(() => info_view.value.url, v => {
|
||||||
|
|
||||||
const changeDisplay = () => display_style.value = display_style.value === 'cards' ? 'list' : 'cards'
|
const changeDisplay = () => display_style.value = display_style.value === 'cards' ? 'list' : 'cards'
|
||||||
|
|
||||||
const toNewDownload = async (item: any) => {
|
const toNewDownload = async (item: item_request) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ const updateData = async notifications => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteItem = async item => {
|
const deleteItem = async item => {
|
||||||
if (true !== box.confirm(`Are you sure you want to delete notification target (${item.name})?`)) {
|
if (true !== box.confirm(`Delete '${item.name}'?`)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,7 +296,7 @@ const editItem = item => {
|
||||||
const join_events = events => (!events || events.length < 1) ? 'ALL' : events.map(e => ucFirst(e)).join(', ')
|
const join_events = events => (!events || events.length < 1) ? 'ALL' : events.map(e => ucFirst(e)).join(', ')
|
||||||
|
|
||||||
const sendTest = async () => {
|
const sendTest = async () => {
|
||||||
if (true !== box.confirm('Are you sure you want to send a test notification?')) {
|
if (true !== box.confirm('Send test notification?')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -385,26 +385,6 @@ const formatBytes = (bytes, decimals = 2) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert options to JSON
|
|
||||||
*
|
|
||||||
* @param {string} opts
|
|
||||||
*
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
const convertCliOptions = async opts => {
|
|
||||||
const response = await request('/api/yt-dlp/convert', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ args: opts }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
if (200 !== response.status) {
|
|
||||||
throw new Error(`Error: (${response.status}): ${data.error}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if array or object has data.
|
* Check if array or object has data.
|
||||||
|
|
@ -513,7 +493,6 @@ export {
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
makeDownload,
|
makeDownload,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
convertCliOptions,
|
|
||||||
has_data,
|
has_data,
|
||||||
toggleClass,
|
toggleClass,
|
||||||
cleanObject,
|
cleanObject,
|
||||||
|
|
|
||||||
45
ui/utils/utils.ts
Normal file
45
ui/utils/utils.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import type { convert_args_response } from "~/@types/responses";
|
||||||
|
|
||||||
|
const separators = [
|
||||||
|
{ name: 'Comma', value: ',', },
|
||||||
|
{ name: 'Semicolon', value: ';', },
|
||||||
|
{ name: 'Colon', value: ':', },
|
||||||
|
{ name: 'Pipe', value: '|', },
|
||||||
|
{ name: 'Space', value: ' ', }
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of a separator based on its value
|
||||||
|
*
|
||||||
|
* @param {string} value - The separator value
|
||||||
|
*
|
||||||
|
* @returns {string} The name of the separator, or 'Unknown' if not found
|
||||||
|
*/
|
||||||
|
const getSeparatorsName = (value: string): string => {
|
||||||
|
const sep = separators.find(s => s.value === value)
|
||||||
|
return sep ? `${sep.name} (${value})` : 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert options to JSON
|
||||||
|
*
|
||||||
|
* @param {string} opts
|
||||||
|
*
|
||||||
|
* @returns {Promise<convert_args_response>} The converted options
|
||||||
|
*/
|
||||||
|
const convertCliOptions = async (opts: string): Promise<convert_args_response> => {
|
||||||
|
const response = await request('/api/yt-dlp/convert', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ args: opts }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (200 !== response.status) {
|
||||||
|
throw new Error(`Error: (${response.status}): ${data.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export { convertCliOptions, separators, getSeparatorsName }
|
||||||
Loading…
Reference in a new issue