migrate more code to ts
This commit is contained in:
parent
b0f08f1531
commit
bdbc2f5b7a
12 changed files with 838 additions and 724 deletions
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
|
|
@ -25,6 +25,7 @@
|
||||||
"copyts",
|
"copyts",
|
||||||
"creationflags",
|
"creationflags",
|
||||||
"cronsim",
|
"cronsim",
|
||||||
|
"dailymotion",
|
||||||
"datas",
|
"datas",
|
||||||
"dateparser",
|
"dateparser",
|
||||||
"daterange",
|
"daterange",
|
||||||
|
|
@ -40,10 +41,12 @@
|
||||||
"forceprint",
|
"forceprint",
|
||||||
"fribidi",
|
"fribidi",
|
||||||
"getpid",
|
"getpid",
|
||||||
|
"gibibytes",
|
||||||
"gpac",
|
"gpac",
|
||||||
"hiddenimports",
|
"hiddenimports",
|
||||||
"hookspath",
|
"hookspath",
|
||||||
"httpx",
|
"httpx",
|
||||||
|
"kibibytes",
|
||||||
"levelno",
|
"levelno",
|
||||||
"libcurl",
|
"libcurl",
|
||||||
"libjavascriptcoregtk",
|
"libjavascriptcoregtk",
|
||||||
|
|
@ -53,6 +56,7 @@
|
||||||
"matroska",
|
"matroska",
|
||||||
"mbed",
|
"mbed",
|
||||||
"mccabe",
|
"mccabe",
|
||||||
|
"mebibytes",
|
||||||
"MEIPASS",
|
"MEIPASS",
|
||||||
"Microformat",
|
"Microformat",
|
||||||
"microformats",
|
"microformats",
|
||||||
|
|
@ -60,6 +64,7 @@
|
||||||
"movflags",
|
"movflags",
|
||||||
"mpegts",
|
"mpegts",
|
||||||
"msvideo",
|
"msvideo",
|
||||||
|
"mult",
|
||||||
"multidict",
|
"multidict",
|
||||||
"muxdelay",
|
"muxdelay",
|
||||||
"nodesc",
|
"nodesc",
|
||||||
|
|
@ -77,6 +82,7 @@
|
||||||
"pypi",
|
"pypi",
|
||||||
"pyside",
|
"pyside",
|
||||||
"pywebview",
|
"pywebview",
|
||||||
|
"qstr",
|
||||||
"qtpy",
|
"qtpy",
|
||||||
"quicktime",
|
"quicktime",
|
||||||
"rejecttitle",
|
"rejecttitle",
|
||||||
|
|
@ -85,17 +91,23 @@
|
||||||
"SIGUSR",
|
"SIGUSR",
|
||||||
"smhd",
|
"smhd",
|
||||||
"socketio",
|
"socketio",
|
||||||
|
"sstr",
|
||||||
|
"tebibytes",
|
||||||
|
"tiktok",
|
||||||
"timespec",
|
"timespec",
|
||||||
"tmpfilename",
|
"tmpfilename",
|
||||||
"ungroup",
|
"ungroup",
|
||||||
|
"unnegated",
|
||||||
"upgrader",
|
"upgrader",
|
||||||
"urandom",
|
"urandom",
|
||||||
"urlsafe",
|
"urlsafe",
|
||||||
"usegmt",
|
"usegmt",
|
||||||
|
"ustr",
|
||||||
"vcodec",
|
"vcodec",
|
||||||
"vconvert",
|
"vconvert",
|
||||||
"writedescription",
|
"writedescription",
|
||||||
"xerror"
|
"xerror",
|
||||||
|
"youtu"
|
||||||
],
|
],
|
||||||
"css.styleSheets": [
|
"css.styleSheets": [
|
||||||
"ui/assets/css/*.css"
|
"ui/assets/css/*.css"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
|
|
@ -185,7 +186,11 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data={"status": status, "condition": cond, "data": data},
|
data={
|
||||||
|
"status": status,
|
||||||
|
"condition": cond,
|
||||||
|
"data": OrderedDict(sorted(data.items(), key=lambda item: len(str(item[1])))),
|
||||||
|
},
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=encoder.encode,
|
dumps=encoder.encode,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -181,23 +181,22 @@
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="is-bold" :class="{
|
<span class="is-bold" :class="{
|
||||||
'has-text-success': true === test_data?.data?.status,
|
'has-text-success': true === logic_test,
|
||||||
'has-text-danger': false === test_data?.data?.status,
|
'has-text-danger': false === logic_test,
|
||||||
}">
|
}">
|
||||||
<span class="icon"><i class="fa-solid" :class="{
|
<span class="icon"><i class="fa-solid" :class="{
|
||||||
'fa-check': true === test_data.data.status,
|
'fa-check': true === logic_test,
|
||||||
'fa-xmark': false === test_data.data.status,
|
'fa-xmark': false === logic_test,
|
||||||
'fa-question': null === (test_data.data.status || null),
|
'fa-question': null === logic_test,
|
||||||
}" /></span>
|
}" /></span>
|
||||||
Filter Status: {{ test_data?.data?.status === null ? 'Not tested' : test_data?.data?.status ?
|
Filter Status:
|
||||||
'Matched' : 'Not matched' }}
|
<template v-if="null === test_data?.data?.status">Not tested</template>
|
||||||
|
<template v-else>{{ logic_test ? 'Matched' : 'Not matched' }}</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<pre style="height:60vh;"><code>{{ show_data() }}</code></pre>
|
<pre style="height:60vh;"><code>{{ show_data() }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -209,6 +208,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { decode } from '~/utils/importer'
|
import { decode } from '~/utils/importer'
|
||||||
|
import { match_str } from '~/utils/ytdlp'
|
||||||
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
|
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
|
||||||
|
|
||||||
const emitter = defineEmits<{
|
const emitter = defineEmits<{
|
||||||
|
|
@ -232,8 +232,11 @@ const test_data = ref<{
|
||||||
show: boolean,
|
show: boolean,
|
||||||
url: string,
|
url: string,
|
||||||
in_progress: boolean,
|
in_progress: boolean,
|
||||||
|
changed: boolean,
|
||||||
data: { status: boolean | null, data: Record<string, any> }
|
data: { status: boolean | null, data: Record<string, any> }
|
||||||
}>({ show: false, url: '', in_progress: false, data: { status: null, data: {} } })
|
}>({ show: false, url: '', in_progress: false, changed: false, data: { status: null, data: {} } })
|
||||||
|
|
||||||
|
watch(() => form.filter, () => test_data.value.changed = true)
|
||||||
|
|
||||||
const checkInfo = async (): Promise<void> => {
|
const checkInfo = async (): Promise<void> => {
|
||||||
const required: (keyof ConditionItem)[] = ['name', 'filter', 'cli']
|
const required: (keyof ConditionItem)[] = ['name', 'filter', 'cli']
|
||||||
|
|
@ -304,6 +307,7 @@ const run_test = async (): Promise<void> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
test_data.value.data = json
|
test_data.value.data = json
|
||||||
|
test_data.value.changed = false
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(`Failed to test condition. ${error.message}`)
|
toast.error(`Failed to test condition. ${error.message}`)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -357,4 +361,21 @@ const show_data = (): string => {
|
||||||
|
|
||||||
return JSON.stringify(test_data.value.data.data, null, 2)
|
return JSON.stringify(test_data.value.data.data, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logic_test = computed(() => {
|
||||||
|
if (!test_data.value.data || !test_data.value.data.data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!test_data.value.changed) {
|
||||||
|
return test_data.value.data.status
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return match_str(form.filter, test_data.value.data.data, true)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@
|
||||||
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 type { item_request } from '~/types/item'
|
||||||
import { getSeparatorsName, separators } from '~/utils/utils'
|
import { getSeparatorsName, separators } from '~/utils/index'
|
||||||
|
|
||||||
const props = defineProps<{ item?: Partial<item_request> }>()
|
const props = defineProps<{ item?: Partial<item_request> }>()
|
||||||
const emitter = defineEmits<{
|
const emitter = defineEmits<{
|
||||||
|
|
@ -245,7 +245,7 @@ const socket = useSocketStore()
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
|
|
||||||
const showAdvanced = useStorage<boolean>('show_advanced', false)
|
const showAdvanced = useStorage<boolean>('show_advanced', false)
|
||||||
const separator = useStorage<string>('url_separator', separators[0].value)
|
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
|
||||||
const auto_start = useStorage<boolean>('auto_start', true)
|
const auto_start = useStorage<boolean>('auto_start', true)
|
||||||
const show_description = useStorage<boolean>('show_description', true)
|
const show_description = useStorage<boolean>('show_description', true)
|
||||||
|
|
||||||
|
|
@ -414,7 +414,7 @@ onMounted(async () => {
|
||||||
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 ?? '.'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -422,7 +422,7 @@ const hasFormatInConfig = computed((): boolean => !!form.value.cli?.match(/(?<!\
|
||||||
|
|
||||||
const filter_presets = (flag: boolean = 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: string | undefined) => config.presets.find(item => item.name === name)
|
const get_preset = (name: string | undefined) => config.presets.find(item => item.name === name)
|
||||||
const expand_description = (e: Event) => toggleClass(e.target, ['is-ellipsis', 'is-pre-wrap'])
|
const expand_description = (e: Event) => toggleClass(e.target as HTMLElement, ['is-ellipsis', 'is-pre-wrap'])
|
||||||
|
|
||||||
const removeFromArchive = async (url: string) => {
|
const removeFromArchive = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
<script setup lang="ts">
|
<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'
|
import { separators } from '~/utils/index'
|
||||||
|
|
||||||
defineProps<{ isLoading: boolean }>()
|
defineProps<{ isLoading: boolean }>()
|
||||||
|
|
||||||
|
|
@ -154,5 +154,5 @@ const reduce_confirm = useStorage<boolean>('reduce_confirm', false)
|
||||||
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
|
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
|
||||||
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
|
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
|
||||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
||||||
const separator = useStorage<string>('url_separator', separators[0].value)
|
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
/**
|
|
||||||
* Cache class
|
|
||||||
*
|
|
||||||
* @class
|
|
||||||
* @param {string} engine - The engine to use for caching. Either 'session' or 'local'.
|
|
||||||
* @throws {Error} If the engine is not supported.
|
|
||||||
*/
|
|
||||||
class Cache {
|
|
||||||
supportedEngines = ['session', 'local']
|
|
||||||
|
|
||||||
constructor(engine = 'session') {
|
|
||||||
if (!this.supportedEngines.includes(engine)) {
|
|
||||||
throw new Error(`Engine '${engine}' not supported.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('session' === engine) {
|
|
||||||
this.storage = window.sessionStorage
|
|
||||||
} else {
|
|
||||||
this.storage = window.localStorage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a value in the cache
|
|
||||||
*
|
|
||||||
* @param key {string}
|
|
||||||
* @param value {*}
|
|
||||||
* @param ttl {number|null} - Time to live in milliseconds. If null, the value will not expire.
|
|
||||||
*/
|
|
||||||
set(key, value, ttl = null) {
|
|
||||||
this.storage.setItem(key, JSON.stringify({value, ttl, time: Date.now()}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a value from the cache
|
|
||||||
*
|
|
||||||
* @param key {string}
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
get(key) {
|
|
||||||
const item = this.storage.getItem(key)
|
|
||||||
if (null === item) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {value, ttl, time} = JSON.parse(item)
|
|
||||||
if (null !== ttl && Date.now() - time > ttl) {
|
|
||||||
this.remove(key)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
} catch (e) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value from the cache
|
|
||||||
*
|
|
||||||
* @param key {string}
|
|
||||||
*/
|
|
||||||
remove(key) {
|
|
||||||
this.storage.removeItem(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the cache
|
|
||||||
*
|
|
||||||
* @param {function|null} filter - A filter function to remove only specific keys.
|
|
||||||
*/
|
|
||||||
clear(filter = null) {
|
|
||||||
if (null !== filter) {
|
|
||||||
Object.keys(this.storage ?? {}).filter(filter).forEach(k => this.storage.removeItem(k))
|
|
||||||
} else {
|
|
||||||
this.storage.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a key is in the cache
|
|
||||||
*
|
|
||||||
* @param key {string}
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
has(key) {
|
|
||||||
return null !== this.get(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useSessionCache = () => new Cache('session')
|
|
||||||
const useLocalCache = () => new Cache('local')
|
|
||||||
|
|
||||||
export {useSessionCache, useLocalCache}
|
|
||||||
64
ui/utils/cache.ts
Normal file
64
ui/utils/cache.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
type CacheEngine = 'session' | 'local'
|
||||||
|
|
||||||
|
interface CachedItem<T = any> {
|
||||||
|
value: T
|
||||||
|
ttl: number | null
|
||||||
|
time: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cache {
|
||||||
|
private storage: Storage
|
||||||
|
private supportedEngines: CacheEngine[] = ['session', 'local']
|
||||||
|
|
||||||
|
constructor(engine: CacheEngine = 'session') {
|
||||||
|
if (!this.supportedEngines.includes(engine)) {
|
||||||
|
throw new Error(`Engine '${engine}' not supported.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage = 'session' === engine ? window.sessionStorage : window.localStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T = any>(key: string, value: T, ttl: number | null = null): void {
|
||||||
|
const item: CachedItem<T> = { value, ttl, time: Date.now() }
|
||||||
|
this.storage.setItem(key, JSON.stringify(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T = any>(key: string): T | null {
|
||||||
|
const item = this.storage.getItem(key)
|
||||||
|
if (null === item) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed: CachedItem<T> = JSON.parse(item)
|
||||||
|
if (null !== parsed.ttl && Date.now() - parsed.time > parsed.ttl) {
|
||||||
|
this.remove(key)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return parsed.value
|
||||||
|
} catch {
|
||||||
|
return item as unknown as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key: string): void {
|
||||||
|
this.storage.removeItem(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(filter: ((key: string) => boolean) | null = null): void {
|
||||||
|
if (null !== filter) {
|
||||||
|
Object.keys(this.storage).filter(filter).forEach(k => this.storage.removeItem(k))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.storage.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: string): boolean {
|
||||||
|
return null !== this.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useLocalCache = (): Cache => new Cache('local')
|
||||||
|
const useSessionCache = (): Cache => new Cache('session')
|
||||||
|
|
||||||
|
export { useSessionCache, useLocalCache, Cache }
|
||||||
|
|
@ -1,79 +1,86 @@
|
||||||
const sources = [
|
export type EmbedSource = {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
regex: RegExp
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources: EmbedSource[] = [
|
||||||
{
|
{
|
||||||
name: 'youtube',
|
name: 'youtube',
|
||||||
url: 'https://www.youtube-nocookie.com/embed/',
|
url: 'https://www.youtube-nocookie.com/embed/{id}',
|
||||||
regex: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:shorts\/|[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{11})/,
|
regex: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:shorts\/|[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{11})/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instagram_post",
|
name: "instagram_post",
|
||||||
url: "https://www.instagram.com/p/",
|
url: "https://www.instagram.com/p/{id}",
|
||||||
regex: /https?:\/\/(?:www\.)?instagram\.com\/p\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?instagram\.com\/p\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "instagram_story",
|
name: "instagram_story",
|
||||||
url: "https://www.instagram.com/stories/highlights/",
|
url: "https://www.instagram.com/stories/highlights/{id}",
|
||||||
regex: /https?:\/\/(?:www\.)?instagram\.com\/stories\/highlights\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?instagram\.com\/stories\/highlights\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "twitter",
|
name: "twitter",
|
||||||
url: "https://twitter.com/i/status/",
|
url: "https://twitter.com/i/status/{id}",
|
||||||
regex: /https?:\/\/(?:www\.)?(twitter\.com|x\.com)\/.+?\/status\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?(twitter\.com|x\.com)\/.+?\/status\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "facebook",
|
name: "facebook",
|
||||||
url: "https://www.facebook.com/plugins/post.php?href=",
|
url: "https://www.facebook.com/plugins/post.php?href={id}",
|
||||||
regex: /https?:\/\/(?:www\.)?facebook\.com\/(?:[^/?#&]+)\/posts\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?facebook\.com\/(?:[^/?#&]+)\/posts\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tiktok",
|
name: "tiktok",
|
||||||
url: "https://www.tiktok.com/embed/",
|
url: "https://www.tiktok.com/embed/{id}",
|
||||||
regex: /https?:\/\/(?:www\.)?tiktok\.com\/(?:[^/?#&]+)\/video\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?tiktok\.com\/(?:[^/?#&]+)\/video\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "vimeo",
|
name: "vimeo",
|
||||||
url: "https://player.vimeo.com/video/",
|
url: "https://player.vimeo.com/video/{id}",
|
||||||
regex: /https?:\/\/(?:www\.)?vimeo\.com(?:\/[^/?#&]+)?\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?vimeo\.com(?:\/[^/?#&]+)?\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "spotify",
|
name: "spotify",
|
||||||
url: "https://open.spotify.com/embed/",
|
url: "https://open.spotify.com/embed/{id}",
|
||||||
regex: /https?:\/\/(?:open\.)?spotify\.com\/(?:[^/?#&]+)\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:open\.)?spotify\.com\/(?:[^/?#&]+)\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "twitch_vod",
|
name: "twitch_vod",
|
||||||
url: "https://player.twitch.tv/?parent={origin}&video=",
|
url: "https://player.twitch.tv/?parent={origin}&video={id}",
|
||||||
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?:[^/?#&]+)\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?:[^/?#&]+)\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "twitch_clip",
|
name: "twitch_clip",
|
||||||
url: "https://clips.twitch.tv/embed?parent={origin}&clip=",
|
url: "https://clips.twitch.tv/embed?parent={origin}&clip={id}",
|
||||||
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?:[^/?#&]+)\/clip\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?:[^/?#&]+)\/clip\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "twitch_channel",
|
name: "twitch_channel",
|
||||||
url: "https://player.twitch.tv/?parent={origin}&channel=",
|
url: "https://player.twitch.tv/?parent={origin}&channel={id}",
|
||||||
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?<id>[^/?#&]+)/,
|
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?<id>[^/?#&]+)/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dailymotion",
|
name: "dailymotion",
|
||||||
url: "https://www.dailymotion.com/embed/video/",
|
url: "https://www.dailymotion.com/embed/video/{id}",
|
||||||
regex: /^.+dailymotion.com\/(video|hub)\/(?<id>[^_]+)[^#]*(#video=([^_&]+))?/,
|
regex: /^.+dailymotion.com\/(video|hub)\/(?<id>[^_]+)[^#]*(#video=([^_&]+))?/,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const isEmbedable = url => {
|
const isEmbedable = (url: string): boolean => sources.some(source => source.regex.test(url))
|
||||||
return sources.some(source => source.regex.test(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEmbedable = url => {
|
const getEmbedable = (url: string): string | null => {
|
||||||
const source = sources.find(source => source.regex.test(url));
|
const source = sources.find(source => source.regex.test(url))
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
source.url.replace(/\{origin\}/g, window.location.origin);
|
const match = source.regex.exec(url)
|
||||||
|
if (!match || !match.groups?.id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return source.url + source.regex.exec(url)['groups'].id;
|
return source.url.replace(/\{origin\}/g, window.location.origin).replace(/\{id\}/g, match.groups.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isEmbedable, getEmbedable };
|
export { isEmbedable, getEmbedable }
|
||||||
|
|
@ -1,502 +0,0 @@
|
||||||
const toast = useNotification()
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
const AG_SEPARATOR = '.'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get value from object or function
|
|
||||||
*
|
|
||||||
* @param {Function|*} obj
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getValue = (obj) => 'function' === typeof obj ? obj() : obj
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get value from object or function and return default value if it's undefined or null
|
|
||||||
*
|
|
||||||
* @param {Object|Array|any} obj The object to get the value from.
|
|
||||||
* @param {string} path The path to the value.
|
|
||||||
* @param {*} defaultValue The default value to return if the path is not found.
|
|
||||||
*
|
|
||||||
* @returns {*} The value at the path or the default value.
|
|
||||||
*/
|
|
||||||
const ag = (obj, path, defaultValue = null) => {
|
|
||||||
const keys = path.split(AG_SEPARATOR)
|
|
||||||
let at = obj
|
|
||||||
|
|
||||||
for (let key of keys) {
|
|
||||||
if (typeof at === 'object' && null !== at && key in at) {
|
|
||||||
at = at[key]
|
|
||||||
} else {
|
|
||||||
return getValue(defaultValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getValue(null === at ? defaultValue : at)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set value in object by path
|
|
||||||
*
|
|
||||||
* @param {Object} obj The object to set the value in.
|
|
||||||
* @param {string} path The path to the value.
|
|
||||||
* @param {*} value The value to set.
|
|
||||||
*
|
|
||||||
* @returns {Object} The object with the value set.
|
|
||||||
*/
|
|
||||||
const ag_set = (obj, path, value) => {
|
|
||||||
const keys = path.split(AG_SEPARATOR)
|
|
||||||
let at = obj
|
|
||||||
|
|
||||||
while (keys.length > 0) {
|
|
||||||
if (keys.length === 1) {
|
|
||||||
if (typeof at === 'object' && at !== null) {
|
|
||||||
at[keys.shift()] = value
|
|
||||||
} else {
|
|
||||||
throw new Error(`Cannot set value at this path (${path}) because it's not an object.`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const key = keys.shift()
|
|
||||||
if (!at[key]) {
|
|
||||||
at[key] = {}
|
|
||||||
}
|
|
||||||
at = at[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for an element to be loaded in the DOM
|
|
||||||
*
|
|
||||||
* @param {string} sel The selector of the element.
|
|
||||||
* @param {Function} callback The callback function.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const awaitElement = (sel, callback) => {
|
|
||||||
let interval = undefined
|
|
||||||
let $elm = document.querySelector(sel)
|
|
||||||
|
|
||||||
if ($elm) {
|
|
||||||
callback($elm, sel)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
interval = setInterval(() => {
|
|
||||||
let $elm = document.querySelector(sel)
|
|
||||||
if ($elm) {
|
|
||||||
clearInterval(interval)
|
|
||||||
callback($elm, sel)
|
|
||||||
}
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace tags in text with values from context
|
|
||||||
*
|
|
||||||
* @param {string} text The text with tags
|
|
||||||
* @param {object} context The context with values
|
|
||||||
*
|
|
||||||
* @returns {string} The text with replaced tags
|
|
||||||
*/
|
|
||||||
const r = (text, context = {}) => {
|
|
||||||
const tagLeft = '{'
|
|
||||||
const tagRight = '}'
|
|
||||||
|
|
||||||
if (!text.includes(tagLeft) || !text.includes(tagRight)) {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = new RegExp(`${tagLeft}([\\w_.]+)${tagRight}`, 'g')
|
|
||||||
const matches = text.match(pattern)
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
let replacements = {}
|
|
||||||
|
|
||||||
matches.forEach(match => replacements[match] = ag(context, match.slice(1, -1), ''))
|
|
||||||
|
|
||||||
for (let key in replacements) {
|
|
||||||
text = text.replace(new RegExp(key, 'g'), replacements[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyText = (str, notify = true, store = false) => {
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
navigator.clipboard.writeText(str).then(() => {
|
|
||||||
if (notify) {
|
|
||||||
toast.success('Text copied to clipboard.')
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Failed to copy.', error)
|
|
||||||
if (notify) {
|
|
||||||
toast.error('Failed to copy to clipboard.')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = document.createElement('textarea')
|
|
||||||
el.value = str
|
|
||||||
document.body.appendChild(el)
|
|
||||||
el.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(el)
|
|
||||||
|
|
||||||
if (notify) {
|
|
||||||
toast.success('Text copied to clipboard.', { store: store })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch custom event.
|
|
||||||
*
|
|
||||||
* @param {string} eventName The name of the event.
|
|
||||||
* @param {object} detail The detail object.
|
|
||||||
*
|
|
||||||
* @returns {Boolean} The return value of dispatchEvent.
|
|
||||||
*/
|
|
||||||
const dEvent = (eventName, detail = {}) => window.dispatchEvent(new CustomEvent(eventName, { detail }))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make pagination
|
|
||||||
*
|
|
||||||
* @param {number} current The current page.
|
|
||||||
* @param {number} last The last page.
|
|
||||||
* @param {number} delta The delta.
|
|
||||||
*
|
|
||||||
* @returns {Array} The pagination array.
|
|
||||||
*/
|
|
||||||
const makePagination = (current, last, delta = 5) => {
|
|
||||||
let pagination = []
|
|
||||||
|
|
||||||
if (last < 2) {
|
|
||||||
return pagination
|
|
||||||
}
|
|
||||||
|
|
||||||
const strR = '-'.repeat(9 + `${last}`.length)
|
|
||||||
|
|
||||||
const left = current - delta, right = current + delta + 1
|
|
||||||
|
|
||||||
for (let i = 1; i <= last; i++) {
|
|
||||||
if (i === 1 || i === last || (i >= left && i < right)) {
|
|
||||||
if (i === left && i > 2) {
|
|
||||||
pagination.push({
|
|
||||||
page: 0, text: strR, selected: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pagination.push({
|
|
||||||
page: i, text: `Page #${i}`, selected: i === current
|
|
||||||
})
|
|
||||||
|
|
||||||
if (i === right - 1 && i < last - 1) {
|
|
||||||
pagination.push({
|
|
||||||
page: 0, text: strR, selected: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pagination
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a path string to be used in a URL
|
|
||||||
*
|
|
||||||
* @param {string} item - The path string to encode
|
|
||||||
*
|
|
||||||
* @returns {string} - The encoded path string
|
|
||||||
*/
|
|
||||||
const encodePath = item => {
|
|
||||||
// -- manually encode #
|
|
||||||
if (!item) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
item = item.replace(/#/g, '%23')
|
|
||||||
try {
|
|
||||||
return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/')
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error encoding path:', e, item)
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request content from the API. This function will automatically add the API token to the request headers.
|
|
||||||
* And prefix the URL with the API URL and path.
|
|
||||||
*
|
|
||||||
* @param {string} url - The URL to request
|
|
||||||
* @param {RequestInit} options - The request options
|
|
||||||
*
|
|
||||||
* @returns {Promise<Response>} - The response from the API
|
|
||||||
*/
|
|
||||||
const request = (url, options = {}) => {
|
|
||||||
options = options || {}
|
|
||||||
options.method = options.method || 'GET'
|
|
||||||
options.headers = options.headers || {}
|
|
||||||
options.withCredentials = true
|
|
||||||
|
|
||||||
if (undefined === options.headers['Content-Type']) {
|
|
||||||
if (!(options?.body instanceof FormData)) {
|
|
||||||
options.headers['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (undefined === options.headers['Accept']) {
|
|
||||||
options.headers['Accept'] = 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith('/')) {
|
|
||||||
options.credentials = 'same-origin'
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch(url.startsWith('/') ? uri(url) : url, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the ANSI colors from the text
|
|
||||||
*
|
|
||||||
* @param {string} text - The text to remove the colors from
|
|
||||||
*
|
|
||||||
* @returns {string} - The text without the colors.
|
|
||||||
*/
|
|
||||||
const removeANSIColors = text => text?.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') ?? text
|
|
||||||
|
|
||||||
const dec2hex = dec => dec.toString(16).padStart(2, "0")
|
|
||||||
const makeId = len => Array.from(window.crypto.getRandomValues(new Uint8Array((len || 40) / 2)), dec2hex).join('')
|
|
||||||
|
|
||||||
const basename = (path, ext = '') => {
|
|
||||||
if (!path) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const segments = path.replace(/\\/g, '/').split('/')
|
|
||||||
let base = segments.pop()
|
|
||||||
while (segments.length && base === '') {
|
|
||||||
base = segments.pop()
|
|
||||||
}
|
|
||||||
if (ext && base.endsWith(ext) && base !== ext) {
|
|
||||||
base = base.substring(0, base.length - ext.length)
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirname = filePath => {
|
|
||||||
const lastIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
|
|
||||||
if (-1 === lastIndex) {
|
|
||||||
return '.'
|
|
||||||
}
|
|
||||||
if (0 === lastIndex) {
|
|
||||||
return filePath[0]
|
|
||||||
}
|
|
||||||
return filePath.substring(0, lastIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const iTrim = (str, delim, position = 'both') => {
|
|
||||||
if (!str) {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!delim) {
|
|
||||||
throw new Error('Delimiter is required')
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("]" === delim) {
|
|
||||||
delim = "\\]"
|
|
||||||
}
|
|
||||||
if ("\\" === delim) {
|
|
||||||
delim = "\\\\"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['both', 'start'].includes(position)) {
|
|
||||||
str = str.replace(new RegExp("^[" + delim + "]+", "g"), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['both', 'end'].includes(position)) {
|
|
||||||
str = str.replace(new RegExp("[" + delim + "]+$", "g"), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
const eTrim = (str, delim) => iTrim(str, delim, 'end')
|
|
||||||
const sTrim = (str, delim) => iTrim(str, delim, 'start')
|
|
||||||
|
|
||||||
const ucFirst = str => (!str) ? str : str.charAt(0).toUpperCase() + str.slice(1)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get query parameters from the URL
|
|
||||||
*
|
|
||||||
* @param {string} url - The URL to get the query parameters from
|
|
||||||
*
|
|
||||||
* @returns {Object} - The query parameters
|
|
||||||
*/
|
|
||||||
const getQueryParams = (url = window.location.search) => Object.fromEntries(new URLSearchParams(url).entries())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make download URL.
|
|
||||||
*
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} item
|
|
||||||
* @param {string} base
|
|
||||||
*
|
|
||||||
* @returns {string} The download URL
|
|
||||||
*/
|
|
||||||
const makeDownload = (config, item, base = 'api/download') => {
|
|
||||||
let baseDir = 'api/player/m3u8/video/';
|
|
||||||
if ('m3u8' !== base) {
|
|
||||||
baseDir = `${base}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.folder) {
|
|
||||||
item.folder = item.folder.replace(/#/g, '%23');
|
|
||||||
baseDir += item.folder + '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`;
|
|
||||||
|
|
||||||
return uri(('m3u8' === base) ? url + '.m3u8' : url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format a file size
|
|
||||||
*
|
|
||||||
* @param {number} bytes
|
|
||||||
* @param {number} decimals
|
|
||||||
*
|
|
||||||
* @returns {string} The formatted file size
|
|
||||||
*/
|
|
||||||
const formatBytes = (bytes, decimals = 2) => {
|
|
||||||
if (!+bytes) return '0 Bytes'
|
|
||||||
|
|
||||||
const k = 1024
|
|
||||||
const dm = decimals < 0 ? 0 : decimals
|
|
||||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if array or object has data.
|
|
||||||
*
|
|
||||||
* @param {string} item - The item to check
|
|
||||||
* @returns {boolean} - True if the item has data, false otherwise.
|
|
||||||
*/
|
|
||||||
const has_data = item => {
|
|
||||||
if (!item) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
try {
|
|
||||||
item = JSON.parse(item)
|
|
||||||
} catch (e) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof item === 'object') {
|
|
||||||
return Object.keys(item).length > 0
|
|
||||||
}
|
|
||||||
return item.length > 0
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleClass = (target, className) => {
|
|
||||||
if (Array.isArray(className)) {
|
|
||||||
className.forEach(name => toggleClass(target, name))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.classList.contains(className)) {
|
|
||||||
target.classList.remove(className)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
target.classList.add(className)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanObject = (item, fields = []) => {
|
|
||||||
if (!item || typeof item !== 'object' || fields.length < 1) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleaned = {}
|
|
||||||
|
|
||||||
for (const key of Object.keys(item)) {
|
|
||||||
if (false === fields.includes(key)) {
|
|
||||||
cleaned[key] = item[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = u => {
|
|
||||||
if (!u || '/' === runtimeConfig.app.baseURL || !u.startsWith('/')) {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === u.startsWith(runtimeConfig.app.baseURL)) {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
return eTrim(runtimeConfig.app.baseURL, '/') + '/' + sTrim(u, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = seconds => {
|
|
||||||
const hrs = Math.floor(seconds / 3600)
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60)
|
|
||||||
const secs = Math.floor(seconds % 60)
|
|
||||||
|
|
||||||
const pad = (n) => n.toString().padStart(2, '0')
|
|
||||||
|
|
||||||
if (hrs > 0) return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`
|
|
||||||
if (mins > 0) return `${pad(mins)}:${pad(secs)}`
|
|
||||||
return `${secs}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const sleep = (seconds) => new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
|
||||||
|
|
||||||
export {
|
|
||||||
ag_set,
|
|
||||||
ag,
|
|
||||||
awaitElement,
|
|
||||||
copyText,
|
|
||||||
dEvent,
|
|
||||||
makePagination,
|
|
||||||
request,
|
|
||||||
r,
|
|
||||||
encodePath,
|
|
||||||
removeANSIColors,
|
|
||||||
makeId,
|
|
||||||
basename,
|
|
||||||
iTrim,
|
|
||||||
eTrim,
|
|
||||||
sTrim,
|
|
||||||
ucFirst,
|
|
||||||
dirname,
|
|
||||||
getQueryParams,
|
|
||||||
makeDownload,
|
|
||||||
formatBytes,
|
|
||||||
has_data,
|
|
||||||
toggleClass,
|
|
||||||
cleanObject,
|
|
||||||
uri,
|
|
||||||
formatTime,
|
|
||||||
sleep,
|
|
||||||
}
|
|
||||||
599
ui/utils/index.ts
Normal file
599
ui/utils/index.ts
Normal file
|
|
@ -0,0 +1,599 @@
|
||||||
|
import type { convert_args_response } from "~/types/responses";
|
||||||
|
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const toast = useNotification()
|
||||||
|
|
||||||
|
const AG_SEPARATOR = '.'
|
||||||
|
|
||||||
|
const separators = [
|
||||||
|
{ name: 'Comma', value: ',', },
|
||||||
|
{ name: 'Semicolon', value: ';', },
|
||||||
|
{ name: 'Colon', value: ':', },
|
||||||
|
{ name: 'Pipe', value: '|', },
|
||||||
|
{ name: 'Space', value: ' ', }
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value from object or function.
|
||||||
|
*
|
||||||
|
* @param obj - A function or value.
|
||||||
|
* @returns The result of calling the function if it's a function, otherwise the value.
|
||||||
|
*/
|
||||||
|
const getValue = <T>(obj: (() => T) | T): T => {
|
||||||
|
return 'function' === typeof obj ? (obj as () => T)() : obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely get a value from a nested object using a path string. Returns default value if not found.
|
||||||
|
*
|
||||||
|
* @param obj - The object to get the value from.
|
||||||
|
* @param path - Dot-delimited string path (e.g. 'a.b.c').
|
||||||
|
* @param defaultValue - The fallback value if path is not found or value is null/undefined.
|
||||||
|
* @returns The value at the path or the default value.
|
||||||
|
*/
|
||||||
|
const ag = <T = any>(obj: any, path: string, defaultValue: T | null = null): T | null => {
|
||||||
|
const keys = path.split(AG_SEPARATOR)
|
||||||
|
let at = obj
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if ('object' === typeof at && null !== at && key in at) {
|
||||||
|
at = at[key]
|
||||||
|
} else {
|
||||||
|
return getValue(defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getValue(null === at ? defaultValue : at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value in a nested object using a dot-delimited path.
|
||||||
|
*
|
||||||
|
* @param obj - The object to modify.
|
||||||
|
* @param path - Dot-delimited string path (e.g. 'a.b.c').
|
||||||
|
* @param value - The value to set.
|
||||||
|
* @returns The original object with the updated value.
|
||||||
|
* @throws Error if a path segment is not an object.
|
||||||
|
*/
|
||||||
|
const ag_set = (obj: Record<string, any>, path: string, value: any): Record<string, any> => {
|
||||||
|
const keys = path.split(AG_SEPARATOR)
|
||||||
|
let at: any = obj
|
||||||
|
|
||||||
|
while (keys.length > 0) {
|
||||||
|
if (1 === keys.length) {
|
||||||
|
if ('object' === typeof at && null !== at) {
|
||||||
|
at[keys.shift()!] = value
|
||||||
|
} else {
|
||||||
|
throw new Error(`Cannot set value at this path (${path}) because it's not an object.`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const key = keys.shift()!
|
||||||
|
if (!at[key] || 'object' !== typeof at[key]) {
|
||||||
|
at[key] = {}
|
||||||
|
}
|
||||||
|
at = at[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for an element to appear in the DOM, then invoke a callback.
|
||||||
|
*
|
||||||
|
* @param sel - CSS selector for the target element.
|
||||||
|
* @param callback - Function to execute when the element is found.
|
||||||
|
*/
|
||||||
|
const awaitElement = (sel: string, callback: (el: Element, selector: string) => void): void => {
|
||||||
|
const $elm = document.querySelector(sel)
|
||||||
|
if ($elm) {
|
||||||
|
callback($elm, sel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const $elm = document.querySelector(sel)
|
||||||
|
if ($elm) {
|
||||||
|
clearInterval(interval)
|
||||||
|
callback($elm, sel)
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace template tags in a string with values from a context object.
|
||||||
|
*
|
||||||
|
* @param text - The input string containing tags in `{key}` format.
|
||||||
|
* @param context - An object whose keys are used for tag replacement.
|
||||||
|
* @returns The string with all matching tags replaced.
|
||||||
|
*/
|
||||||
|
const r = (text: string, context: Record<string, any> = {}): string => {
|
||||||
|
const tagLeft = '{'
|
||||||
|
const tagRight = '}'
|
||||||
|
|
||||||
|
if (!text.includes(tagLeft) || !text.includes(tagRight)) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = new RegExp(`${tagLeft}([\\w_.]+)${tagRight}`, 'g')
|
||||||
|
const matches = text.match(pattern)
|
||||||
|
if (!matches) return text
|
||||||
|
|
||||||
|
const replacements: Record<string, string> = {}
|
||||||
|
matches.forEach(match => {
|
||||||
|
const key = match.slice(1, -1)
|
||||||
|
replacements[match] = String(ag(context, key, ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const key in replacements) {
|
||||||
|
text = text.replace(new RegExp(key, 'g'), String(replacements[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a custom event on the global window object.
|
||||||
|
*
|
||||||
|
* @param eventName - The name of the custom event.
|
||||||
|
* @param detail - Optional detail payload to include in the event.
|
||||||
|
* @returns Whether the event was successfully dispatched.
|
||||||
|
*/
|
||||||
|
const dEvent = (eventName: string, detail: Record<string, any> = {}): boolean => {
|
||||||
|
return window.dispatchEvent(new CustomEvent(eventName, { detail }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a pagination list based on current page, total pages, and delta range.
|
||||||
|
*
|
||||||
|
* @param current - The current active page number.
|
||||||
|
* @param last - The last page number.
|
||||||
|
* @param delta - How many pages to show before/after the current page.
|
||||||
|
* @returns An array of pagination entries including optional gaps.
|
||||||
|
*/
|
||||||
|
const makePagination = (current: number, last: number, delta: number = 5): Array<{ page: number; text: string; selected: boolean }> => {
|
||||||
|
const pagination: Array<{ page: number; text: string; selected: boolean }> = []
|
||||||
|
if (last < 2) {
|
||||||
|
return pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
const strR = '-'.repeat(9 + `${last}`.length)
|
||||||
|
const left = current - delta
|
||||||
|
const right = current + delta + 1
|
||||||
|
|
||||||
|
for (let i = 1; i <= last; i++) {
|
||||||
|
if (1 === i || last === i || (i >= left && i < right)) {
|
||||||
|
if (i === left && i > 2) {
|
||||||
|
pagination.push({ page: 0, text: strR, selected: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination.push({ page: i, text: `Page #${i}`, selected: i === current })
|
||||||
|
|
||||||
|
if (i === right - 1 && i < last - 1) {
|
||||||
|
pagination.push({ page: 0, text: strR, selected: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely encode a path string for use in a URL.
|
||||||
|
* Will manually encode '#' and ensure valid URI segments.
|
||||||
|
*
|
||||||
|
* @param item - The input path string.
|
||||||
|
* @returns The URL-encoded path.
|
||||||
|
*/
|
||||||
|
const encodePath = (item: string): string => {
|
||||||
|
if (!item) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
item = item.replace(/#/g, '%23')
|
||||||
|
|
||||||
|
try {
|
||||||
|
return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error encoding path:', e, item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request content from the API with automatic token handling and URL prefixing.
|
||||||
|
*
|
||||||
|
* @param url - The URL to request. If relative, it will be passed through the `uri` helper.
|
||||||
|
* @param options - Optional fetch options, automatically extended with common headers and credentials.
|
||||||
|
* @returns The fetch Response promise.
|
||||||
|
*/
|
||||||
|
const request = (url: string, options: RequestInit = {}): Promise<Response> => {
|
||||||
|
options.method = options.method || 'GET'
|
||||||
|
options.headers = options.headers || {}; (options as any).withCredentials = true
|
||||||
|
|
||||||
|
if (undefined === (options.headers as Record<string, any>)['Content-Type']) {
|
||||||
|
if (!(options?.body instanceof FormData)) {
|
||||||
|
; (options.headers as Record<string, any>)['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === (options.headers as Record<string, any>)['Accept']) {
|
||||||
|
; (options.headers as Record<string, any>)['Accept'] = 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
options.credentials = 'same-origin'
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url.startsWith('/') ? uri(url) : url, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove ANSI color codes from a string.
|
||||||
|
*
|
||||||
|
* @param text - The text potentially containing ANSI codes.
|
||||||
|
* @returns A string without ANSI escape sequences.
|
||||||
|
*/
|
||||||
|
const removeANSIColors = (text: string): string => {
|
||||||
|
return text?.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') ?? text
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a decimal value to a 2-character hex string.
|
||||||
|
*
|
||||||
|
* @param dec - Decimal value between 0-255.
|
||||||
|
* @returns The hex string.
|
||||||
|
*/
|
||||||
|
const dec2hex = (dec: number): string => dec.toString(16).padStart(2, '0')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random ID using crypto.
|
||||||
|
*
|
||||||
|
* @param len - The length of the ID in characters (must be even).
|
||||||
|
* @returns A random hex ID string.
|
||||||
|
*/
|
||||||
|
const makeId = (len: number = 40): string => Array.from(window.crypto.getRandomValues(new Uint8Array(len / 2)), dec2hex).join('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the basename of a given path.
|
||||||
|
*
|
||||||
|
* @param path - The input path.
|
||||||
|
* @param ext - Optional extension to strip from the basename.
|
||||||
|
* @returns The last segment of the path, minus the extension if matched.
|
||||||
|
*/
|
||||||
|
const basename = (path: string, ext: string = ''): string => {
|
||||||
|
if (!path) return ''
|
||||||
|
const segments = path.replace(/\\/g, '/').split('/')
|
||||||
|
let base = segments.pop() || ''
|
||||||
|
while (segments.length && base === '') {
|
||||||
|
base = segments.pop() || ''
|
||||||
|
}
|
||||||
|
if (ext && base.endsWith(ext) && base !== ext) {
|
||||||
|
base = base.substring(0, base.length - ext.length)
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the directory portion of a path.
|
||||||
|
*
|
||||||
|
* @param filePath - The input file path.
|
||||||
|
* @returns The directory part of the path.
|
||||||
|
*/
|
||||||
|
const dirname = (filePath: string): string => {
|
||||||
|
const lastIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
|
||||||
|
if (-1 === lastIndex) {
|
||||||
|
return '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === lastIndex) {
|
||||||
|
return filePath[0] ?? '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath.substring(0, lastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard with optional notification and storage flag.
|
||||||
|
*
|
||||||
|
* @param str - The string to copy.
|
||||||
|
* @param notify - Whether to show a toast notification (default: true).
|
||||||
|
* @param store - Whether to persist the notification (optional).
|
||||||
|
*/
|
||||||
|
const copyText = (str: string, notify: boolean = true, store: boolean = false): void => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(str).then(() => {
|
||||||
|
if (notify) toast.success('Text copied to clipboard.')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to copy.', error)
|
||||||
|
if (notify) toast.error('Failed to copy to clipboard.')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.createElement('textarea')
|
||||||
|
el.value = str
|
||||||
|
document.body.appendChild(el)
|
||||||
|
el.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(el)
|
||||||
|
|
||||||
|
if (notify) {
|
||||||
|
toast.success('Text copied to clipboard.', { store })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim delimiter characters from a string at specified positions.
|
||||||
|
*
|
||||||
|
* @param str - The input string to trim.
|
||||||
|
* @param delim - The delimiter character to trim.
|
||||||
|
* @param position - Where to trim the delimiter from: 'start', 'end', or 'both'. Defaults to 'both'.
|
||||||
|
* @returns The trimmed string.
|
||||||
|
* @throws Will throw an error if `delim` is not provided.
|
||||||
|
*/
|
||||||
|
const iTrim = (str: string, delim: string, position: 'start' | 'end' | 'both' = 'both'): string => {
|
||||||
|
if (!str) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!delim) {
|
||||||
|
throw new Error('Delimiter is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (']' === delim) {
|
||||||
|
delim = '\\]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('\\' === delim) {
|
||||||
|
delim = '\\\\'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['both', 'start'].includes(position)) {
|
||||||
|
str = str.replace(new RegExp(`^[${delim}]+`, 'g'), '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['both', 'end'].includes(position)) {
|
||||||
|
str = str.replace(new RegExp(`[${delim}]+$`, 'g'), '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim delimiter characters from the end of a string.
|
||||||
|
*
|
||||||
|
* @param str - The input string to trim.
|
||||||
|
* @param delim - The delimiter character to trim.
|
||||||
|
* @returns The trimmed string.
|
||||||
|
*/
|
||||||
|
const eTrim = (str: string, delim: string): string => iTrim(str, delim, 'end')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim delimiter characters from the start of a string.
|
||||||
|
*
|
||||||
|
* @param str - The input string to trim.
|
||||||
|
* @param delim - The delimiter character to trim.
|
||||||
|
* @returns The trimmed string.
|
||||||
|
*/
|
||||||
|
const sTrim = (str: string, delim: string): string => iTrim(str, delim, 'start')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uppercase the first character of the string.
|
||||||
|
*
|
||||||
|
* @param str - The input string.
|
||||||
|
* @returns The string with the first character capitalized.
|
||||||
|
*/
|
||||||
|
const ucFirst = (str: string): string => (!str) ? str : str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query parameters from a URL string or the current location.
|
||||||
|
*
|
||||||
|
* @param url - The full URL or search string (default: current URL's search).
|
||||||
|
* @returns A key-value map of query parameters.
|
||||||
|
*/
|
||||||
|
const getQueryParams = (url: string = window.location.search): Record<string, string> => {
|
||||||
|
return Object.fromEntries(new URLSearchParams(url).entries())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a download URL based on config and item metadata.
|
||||||
|
*
|
||||||
|
* @param config - The application config object.
|
||||||
|
* @param item - The item containing filename/folder.
|
||||||
|
* @param base - The base endpoint type (default: 'api/download').
|
||||||
|
* @returns The fully constructed download URI.
|
||||||
|
*/
|
||||||
|
const makeDownload = (config: any, item: { folder?: string; filename: string }, base: string = 'api/download'): string => {
|
||||||
|
let baseDir = 'api/player/m3u8/video/'
|
||||||
|
if ('m3u8' !== base) {
|
||||||
|
baseDir = `${base}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.folder) {
|
||||||
|
item.folder = item.folder.replace(/#/g, '%23')
|
||||||
|
baseDir += item.folder + '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`
|
||||||
|
return uri('m3u8' === base ? `${url}.m3u8` : url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes to a human-readable string.
|
||||||
|
*
|
||||||
|
* @param bytes - The number of bytes.
|
||||||
|
* @param decimals - Number of decimal places to include.
|
||||||
|
* @returns A formatted size string (e.g., '2.00 MiB').
|
||||||
|
*/
|
||||||
|
const formatBytes = (bytes: number, decimals: number = 2): string => {
|
||||||
|
if (!+bytes) {
|
||||||
|
return '0 Bytes'
|
||||||
|
}
|
||||||
|
const k = 1024
|
||||||
|
const dm = decimals < 0 ? 0 : decimals
|
||||||
|
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if input has non-empty data.
|
||||||
|
*
|
||||||
|
* @param item - The input (object, array, or JSON string).
|
||||||
|
* @returns True if it contains data, false otherwise.
|
||||||
|
*/
|
||||||
|
const has_data = (item: any): boolean => {
|
||||||
|
if (!item) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('string' === typeof item) {
|
||||||
|
try {
|
||||||
|
item = JSON.parse(item)
|
||||||
|
} catch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ('object' === typeof item) return Object.keys(item).length > 0
|
||||||
|
return item.length > 0
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a class on a DOM element. Supports array of class names.
|
||||||
|
*
|
||||||
|
* @param target - The HTML element to toggle classes on.
|
||||||
|
* @param className - Class name or array of class names.
|
||||||
|
*/
|
||||||
|
const toggleClass = (target: HTMLElement, className: string | string[]): void => {
|
||||||
|
if (Array.isArray(className)) {
|
||||||
|
className.forEach(cls => toggleClass(target, cls))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.classList.contains(className)) {
|
||||||
|
target.classList.remove(className)
|
||||||
|
} else {
|
||||||
|
target.classList.add(className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove specified fields from an object.
|
||||||
|
*
|
||||||
|
* @param item - Input object.
|
||||||
|
* @param fields - Keys to exclude.
|
||||||
|
* @returns A new object without the excluded keys.
|
||||||
|
*/
|
||||||
|
const cleanObject = <T extends Record<string, any>>(item: T, fields: string[] = []): Partial<T> => {
|
||||||
|
if (!item || typeof item !== 'object' || fields.length < 1) return item
|
||||||
|
const cleaned: Partial<T> = {}
|
||||||
|
for (const key of Object.keys(item)) {
|
||||||
|
if (!fields.includes(key)) {
|
||||||
|
cleaned[key as keyof T] = item[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix URL with baseURL if needed.
|
||||||
|
*
|
||||||
|
* @param u - The input path.
|
||||||
|
* @returns The fully prefixed URI.
|
||||||
|
*/
|
||||||
|
const uri = (u: string): string => {
|
||||||
|
if (!u || '/' === runtimeConfig.app.baseURL || !u.startsWith('/')) {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u.startsWith(runtimeConfig.app.baseURL)) {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${eTrim(runtimeConfig.app.baseURL, '/')}/${sTrim(u, '/')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format seconds into HH:MM:SS or MM:SS.
|
||||||
|
*
|
||||||
|
* @param seconds - Time in seconds.
|
||||||
|
* @returns A time string.
|
||||||
|
*/
|
||||||
|
const formatTime = (seconds: number): string => {
|
||||||
|
const hrs = Math.floor(seconds / 3600)
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
const pad = (n: number): string => n.toString().padStart(2, '0')
|
||||||
|
|
||||||
|
if (hrs > 0) {
|
||||||
|
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mins > 0) {
|
||||||
|
return `${pad(mins)}:${pad(secs)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${secs}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause execution for a number of seconds.
|
||||||
|
*
|
||||||
|
* @param seconds - Number of seconds to sleep.
|
||||||
|
* @returns A promise that resolves after the delay.
|
||||||
|
*/
|
||||||
|
const sleep = (seconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||||
|
|
||||||
|
export {
|
||||||
|
convertCliOptions, separators, getSeparatorsName, iTrim, eTrim, sTrim, ucFirst,
|
||||||
|
getValue, ag, ag_set, awaitElement, r, copyText, dEvent, makePagination, encodePath,
|
||||||
|
request, removeANSIColors, dec2hex, makeId, basename, dirname, getQueryParams,
|
||||||
|
makeDownload,
|
||||||
|
formatBytes,
|
||||||
|
has_data,
|
||||||
|
toggleClass,
|
||||||
|
cleanObject,
|
||||||
|
uri,
|
||||||
|
formatTime,
|
||||||
|
sleep
|
||||||
|
}
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
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 }
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
const NUMBER_RE = /\d+(?:\.\d+)?/;
|
const NUMBER_RE = /\d+(?:\.\d+)?/;
|
||||||
|
|
||||||
function match_str(filterStr: string, dct: Record<string, any>, incomplete: boolean | Set<string> = false): boolean {
|
function match_str(filterStr: string, dct: Record<string, any>, incomplete: boolean | Set<string> = false): boolean {
|
||||||
|
if (!filterStr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return filterStr
|
return filterStr
|
||||||
.split(/(?<!\\)&/)
|
.split(/(?<!\\)&/)
|
||||||
.every(filterPart => _match_one(filterPart.replace(/\\&/g, '&'), dct, incomplete));
|
.every(filterPart => _match_one(filterPart.replace(/\\&/g, '&'), dct, incomplete));
|
||||||
|
|
@ -9,15 +13,22 @@ function match_str(filterStr: string, dct: Record<string, any>, incomplete: bool
|
||||||
function lookup_unit_table(unitTable: Record<string, number>, s: string, strict = false): number | null {
|
function lookup_unit_table(unitTable: Record<string, number>, s: string, strict = false): number | null {
|
||||||
const numRe = strict ? NUMBER_RE : /\d+[,.]?\d*/;
|
const numRe = strict ? NUMBER_RE : /\d+[,.]?\d*/;
|
||||||
const unitsRe = Object.keys(unitTable).map(u => u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
const unitsRe = Object.keys(unitTable).map(u => u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
||||||
const re = new RegExp(`^(?<num>${numRe.source})\s*(?<unit>${unitsRe})\b`, strict ? '' : 'i');
|
const re = new RegExp(`^(?<num>${numRe.source})\\s*(?<unit>${unitsRe})\\b`, strict ? '' : 'i');
|
||||||
|
|
||||||
const m = s.match(re);
|
const m = s.match(re);
|
||||||
if (!m || !m.groups) return null;
|
if (!m?.groups?.num || !m.groups.unit) {
|
||||||
|
return null;
|
||||||
|
|
||||||
const num = parseFloat(m.groups.num.replace(',', '.'));
|
}
|
||||||
const mult = unitTable[m.groups.unit];
|
|
||||||
|
|
||||||
return Math.round(num * mult);
|
const numStr = m.groups.num.replace(',', '.');
|
||||||
|
const unit = m.groups.unit;
|
||||||
|
const mult = unitTable[unit];
|
||||||
|
if (undefined === mult) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(parseFloat(numStr) * mult);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse_filesize(s: string | null): number | null {
|
function parse_filesize(s: string | null): number | null {
|
||||||
|
|
@ -35,73 +46,109 @@ function parse_filesize(s: string | null): number | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse_duration(s: string): number | null {
|
function parse_duration(s: string): number | null {
|
||||||
if (!s.trim()) return null;
|
if (!s.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const durationRe = /^(?:(?:(?<days>\d+):)?(?<hours>\d+):)?(?<mins>\d+):(?<secs>\d{1,2})(?<ms>[.:]\d+)?Z?$/;
|
const durationRe = /^(?:(?:(?<days>\d+):)?(?<hours>\d+):)?(?<mins>\d+):(?<secs>\d{1,2})(?<ms>[.:]\d+)?Z?$/;
|
||||||
const m = s.match(durationRe);
|
const m = s.match(durationRe);
|
||||||
if (!m || !m.groups) return null;
|
if (!m?.groups) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { days, hours, mins, secs, ms } = m.groups;
|
const { days, hours, mins, secs, ms } = m.groups;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(parseFloat(days || '0') * 86400) +
|
(parseFloat(days ?? '0') * 86400) +
|
||||||
(parseFloat(hours || '0') * 3600) +
|
(parseFloat(hours ?? '0') * 3600) +
|
||||||
(parseFloat(mins || '0') * 60) +
|
(parseFloat(mins ?? '0') * 60) + parseFloat(secs ?? '0') + parseFloat((ms ?? '0').replace(':', '.'))
|
||||||
parseFloat(secs || '0') +
|
|
||||||
parseFloat((ms || '0').replace(':', '.'))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _match_one(filterPart: string, dct: Record<string, any>, incomplete: boolean | Set<string>): boolean {
|
function _match_one(filterPart: string, dct: Record<string, any>, incomplete: boolean | Set<string>): boolean {
|
||||||
const STRING_OPERATORS: Record<string, Function> = {
|
const STRING_OPERATORS: Record<string, (a: string, b: string) => boolean> = {
|
||||||
'*=': (a: string, v: string) => a.includes(v),
|
'*=': (a, b) => a.includes(b),
|
||||||
'^=': (a: string, v: string) => a.startsWith(v),
|
'^=': (a, b) => a.startsWith(b),
|
||||||
'$=': (a: string, v: string) => a.endsWith(v),
|
'$=': (a, b) => a.endsWith(b),
|
||||||
'~=': (a: string, v: string) => new RegExp(v).test(a),
|
'~=': (a, b) => new RegExp(b).test(a),
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMPARISON_OPERATORS: Record<string, Function> = {
|
const COMPARISON_OPERATORS: Record<string, (a: any, b: any) => boolean> = {
|
||||||
...STRING_OPERATORS,
|
'*=': STRING_OPERATORS['*=']!,
|
||||||
'<=': (a: any, b: any) => a <= b,
|
'^=': STRING_OPERATORS['^=']!,
|
||||||
'<': (a: any, b: any) => a < b,
|
'$=': STRING_OPERATORS['$=']!,
|
||||||
'>=': (a: any, b: any) => a >= b,
|
'~=': STRING_OPERATORS['~=']!,
|
||||||
'>': (a: any, b: any) => a > b,
|
'<=': (a, b) => a <= b,
|
||||||
'=': (a: any, b: any) => a == b,
|
'<': (a, b) => a < b,
|
||||||
|
'>=': (a, b) => a >= b,
|
||||||
|
'>': (a, b) => a > b,
|
||||||
|
'=': (a, b) => a == b,
|
||||||
};
|
};
|
||||||
|
|
||||||
const match = filterPart.trim().match(/^([a-z_]+)\s*(!)?\s*([*^$~=<>]+)\s*(?:"(.+)"|'(.+)'|(.+))$/);
|
const isIncomplete = typeof incomplete === 'boolean' ? (_: string) => incomplete : (k: string) => incomplete.has(k);
|
||||||
|
|
||||||
if (match) {
|
const cmpOps = Object.keys(COMPARISON_OPERATORS).map(op => op.replace(/([.*+?^${}()|\[\]\\])/g, '\\$1')).join('|');
|
||||||
const [, key, negation, op, qstr, sstr, ustr] = match;
|
const operatorRe = new RegExp(`^(?<key>[a-z_]+)\\s*(?<negation>!\\s*)?(?<op>${cmpOps})(?<noneInclusive>\\s*\\?)?\\s*(?:(?<quote>["'])(?<quoted>.+?)(?:\\k<quote>)|(?<plain>.+))$`);
|
||||||
const value = qstr || sstr || ustr;
|
|
||||||
const actual = dct[key];
|
|
||||||
|
|
||||||
const compValue = parse_filesize(value) || parse_duration(value) || value;
|
const m = filterPart.trim().match(operatorRe);
|
||||||
|
if (m?.groups) {
|
||||||
|
const { key, negation, op, noneInclusive, quote, quoted, plain } = m.groups;
|
||||||
|
const unnegatedOp = COMPARISON_OPERATORS[op!];
|
||||||
|
if (!unnegatedOp) {
|
||||||
|
throw new Error(`Invalid operator '${op}'`);
|
||||||
|
}
|
||||||
|
|
||||||
const _cmp = COMPARISON_OPERATORS[op];
|
const opFn = negation ? (a: any, b: any) => !unnegatedOp(a, b) : unnegatedOp;
|
||||||
if (actual === undefined) return incomplete === true || (incomplete instanceof Set && incomplete.has(key));
|
|
||||||
|
|
||||||
return negation ? !_cmp(actual, compValue) : _cmp(actual, compValue);
|
let value = quoted ?? plain ?? '';
|
||||||
|
if (quote) value = value.replace(new RegExp(`\\\\${quote}`, 'g'), quote);
|
||||||
|
|
||||||
|
const actual = dct[key!];
|
||||||
|
let numeric: number | null = null;
|
||||||
|
|
||||||
|
if (typeof actual === 'number') {
|
||||||
|
numeric = Number(value);
|
||||||
|
if (Number.isNaN(numeric)) {
|
||||||
|
numeric = parse_filesize(value) ?? parse_filesize(`${value}B`) ?? parse_duration(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numeric !== null && op! in STRING_OPERATORS) {
|
||||||
|
throw new Error(`Operator ${op} only supports string values!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual == null) {
|
||||||
|
return isIncomplete(key!) || Boolean(noneInclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
return opFn(actual, numeric !== null ? numeric : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const UNARY_OPERATORS: Record<string, Function> = {
|
const UNARY_OPERATORS: Record<string, (v: any) => boolean> = {
|
||||||
'': (v: any) => v != null,
|
'': v => typeof v === 'boolean' ? v === true : v != null,
|
||||||
'!': (v: any) => v == null,
|
'!': v => typeof v === 'boolean' ? v === false : v == null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unaryMatch = filterPart.trim().match(/^(!)?([a-z_]+)$/);
|
const unaryRe = new RegExp(`^(?<op>!?)\\s*(?<key>[a-z_]+)$`);
|
||||||
|
const mu = filterPart.trim().match(unaryRe);
|
||||||
|
if (mu?.groups) {
|
||||||
|
const { op, key } = mu.groups;
|
||||||
|
const actual = dct[key!];
|
||||||
|
|
||||||
if (unaryMatch) {
|
if (actual === undefined && isIncomplete(key!)) {
|
||||||
const [, op, key] = unaryMatch;
|
return true;
|
||||||
const actual = dct[key];
|
}
|
||||||
|
|
||||||
if (actual === undefined && (incomplete === true || (incomplete instanceof Set && incomplete.has(key)))) return true;
|
const unaryOp = UNARY_OPERATORS[op!];
|
||||||
|
|
||||||
return UNARY_OPERATORS[op || ''](actual);
|
if (!unaryOp) {
|
||||||
|
throw new Error(`Invalid unary operator '${op}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unaryOp(actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Invalid filter part '${filterPart}'`);
|
throw new Error(`Invalid filter part '${filterPart}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export { match_str, lookup_unit_table, parse_filesize, parse_duration, _match_one }
|
export { match_str, lookup_unit_table, parse_filesize, parse_duration, _match_one }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue