Merge pull request #378 from arabcoders/dev
Some checks failed
Build WebView wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build WebView wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, windows-latest) (push) Has been cancelled
Some checks failed
Build WebView wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build WebView wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, windows-latest) (push) Has been cancelled
Added built-in reference for yt-dlp options.
This commit is contained in:
commit
b4505d2fcd
15 changed files with 527 additions and 63 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -98,6 +98,7 @@
|
||||||
"smhd",
|
"smhd",
|
||||||
"socketio",
|
"socketio",
|
||||||
"sstr",
|
"sstr",
|
||||||
|
"SUPPRESSHELP",
|
||||||
"tebibytes",
|
"tebibytes",
|
||||||
"tiktok",
|
"tiktok",
|
||||||
"timespec",
|
"timespec",
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,21 @@ REMOVE_KEYS: list = [
|
||||||
"progress_template": "--progress-template",
|
"progress_template": "--progress-template",
|
||||||
"consoletitle": "--console-title",
|
"consoletitle": "--console-title",
|
||||||
"progress_with_newline": "--newline",
|
"progress_with_newline": "--newline",
|
||||||
"forcejson": "-j, --dump-json",
|
"forcejson": "-j, --dump-single-json",
|
||||||
|
"opt_update_to": "--update-to",
|
||||||
|
"opt_ap_list_mso": "--ap-list-mso",
|
||||||
|
"opt_batch_file": "-a, --batch-file",
|
||||||
|
"opt_alias": "--alias",
|
||||||
|
"opt_list_extractors": "--list-extractors",
|
||||||
|
"opt_version": "--version",
|
||||||
|
"opt_help": "-h, --help",
|
||||||
|
"opt_update": "-U, --update",
|
||||||
|
"opt_list_subtitles": "--list-subs",
|
||||||
|
"opt_list_thumbnails": "--list-thumbnails",
|
||||||
|
"opt_list_format": "-F, --list-formats",
|
||||||
|
"opt_dump_agent": "--dump-user-agent",
|
||||||
|
"opt_extractor_descriptions": "--extractor-descriptions",
|
||||||
|
"opt_list_impersonate_targets": "--list-impersonate-targets"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
|
||||||
import app.postprocessors # noqa: F401
|
import app.postprocessors # noqa: F401
|
||||||
|
|
@ -12,3 +16,44 @@ class YTDLP(yt_dlp.YoutubeDL):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return super()._delete_downloaded_files(*args, **kwargs)
|
return super()._delete_downloaded_files(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def ytdlp_options() -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Collect yt-dlp options and return them in a structured format.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict[str, Any]]: A list of dictionaries containing option flags, descriptions,
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yt_dlp.options import create_parser
|
||||||
|
|
||||||
|
from app.library.Utils import REMOVE_KEYS
|
||||||
|
|
||||||
|
parser = create_parser()
|
||||||
|
|
||||||
|
ignored_flags: set[str] = {
|
||||||
|
f.strip() for group in REMOVE_KEYS for v in group.values() for f in v.split(",") if f.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
def collect(opts, group: str) -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"flags": list(getattr(opt, "_short_opts", [])) + list(getattr(opt, "_long_opts", [])),
|
||||||
|
"description": getattr(opt, "help", None),
|
||||||
|
"group": group,
|
||||||
|
"ignored": any(
|
||||||
|
f in ignored_flags for f in getattr(opt, "_short_opts", []) + getattr(opt, "_long_opts", [])
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for opt in opts
|
||||||
|
if (
|
||||||
|
(getattr(opt, "_short_opts", []) or getattr(opt, "_long_opts", []))
|
||||||
|
and getattr(opt, "help", None)
|
||||||
|
and "SUPPRESSHELP" not in getattr(opt, "help", "")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return collect(parser.option_list, "root") + [
|
||||||
|
entry for grp in parser.option_groups for entry in collect(grp.option_list, grp.title or "other")
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response:
|
||||||
"ttl_left": data.get("_cached", {}).get("expires", time.time() + 300) - time.time(),
|
"ttl_left": data.get("_cached", {}).get("expires", time.time() + 300) - time.time(),
|
||||||
"expires": data.get("_cached", {}).get("expires", time.time() + 300),
|
"expires": data.get("_cached", {}).get("expires", time.time() + 300),
|
||||||
}
|
}
|
||||||
return web.Response(body=json.dumps(data, indent=4, default=str), status=web.HTTPOk.status_code)
|
return web.json_response(body=json.dumps(data, indent=4, default=str), status=web.HTTPOk.status_code)
|
||||||
|
|
||||||
if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None):
|
if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None):
|
||||||
opts = opts.add({"proxy": ytdlp_proxy})
|
opts = opts.add({"proxy": ytdlp_proxy})
|
||||||
|
|
@ -203,7 +203,7 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response:
|
||||||
|
|
||||||
cache.set(key=key, value=data, ttl=300)
|
cache.set(key=key, value=data, ttl=300)
|
||||||
|
|
||||||
return web.Response(body=json.dumps(data, indent=4, default=str), status=web.HTTPOk.status_code)
|
return web.json_response(body=json.dumps(data, indent=4, default=str), status=web.HTTPOk.status_code)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.error(f"Error encountered while getting video info for '{url}'. '{e!s}'.")
|
LOG.error(f"Error encountered while getting video info for '{url}'. '{e!s}'.")
|
||||||
|
|
@ -296,3 +296,19 @@ async def archive_recheck(cache: Cache) -> Response:
|
||||||
response.append({id: bool(data.get("id", None)) if isinstance(data, dict) else False})
|
response.append({id: bool(data.get("id", None)) if isinstance(data, dict) else False})
|
||||||
|
|
||||||
return web.json_response(data=response, status=web.HTTPOk.status_code)
|
return web.json_response(data=response, status=web.HTTPOk.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@route("GET", "api/yt-dlp/options/", "get_options")
|
||||||
|
async def get_options() -> Response:
|
||||||
|
"""
|
||||||
|
Get the yt-dlp CLI options.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: The response object with the yt-dlp CLI options.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from app.library.ytdlp import ytdlp_options
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
body=json.dumps(ytdlp_options(), indent=4, default=str), status=web.HTTPOk.status_code
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -357,3 +357,10 @@ table.is-fixed {
|
||||||
div.is-centered {
|
div.is-centered {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-content-max {
|
||||||
|
width: max-content;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress"
|
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress"
|
||||||
placeholder="For the problematic channel or video name.">
|
placeholder="For the problematic channel or video name.">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>The name that refers to this condition.</span>
|
<span>The name that refers to this condition.</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="addInProgress"
|
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="addInProgress"
|
||||||
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'">
|
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>yt-dlp <code>[--match-filters]</code> logic.</span>
|
<span>yt-dlp <code>[--match-filters]</code> logic.</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -92,19 +92,20 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="cli_options">
|
<label class="label is-inline" for="cli_options">
|
||||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
Command options for yt-dlp
|
<span>
|
||||||
|
Command options for yt-dlp -
|
||||||
|
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
||||||
placeholder="command options to use, e.g. --proxy 1.2.3.4:3128" />
|
placeholder="command options to use, e.g. --proxy 1.2.3.4:3128" />
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>If the filter is matched, these options will be used.
|
<span>Not all options are supported <NuxtLink target="_blank"
|
||||||
<span class="has-text-danger">This will override the command options for yt-dlp given with the
|
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
|
||||||
URL. it's recommended to use presets and keep that field with url empty if you plan to use this
|
ignored</NuxtLink>. Use with caution.</span>
|
||||||
feature.</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -202,6 +203,9 @@
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
||||||
|
<YTDLPOptions />
|
||||||
|
</Modal>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -233,6 +237,7 @@ const test_data = ref<{
|
||||||
changed: 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, changed: false, data: { status: null, data: {} } })
|
}>({ show: false, url: '', in_progress: false, changed: false, data: { status: null, data: {} } })
|
||||||
|
const showOptions = ref<boolean>(false)
|
||||||
|
|
||||||
watch(() => form.filter, () => test_data.value.changed = true)
|
watch(() => form.filter, () => test_data.value.changed = true)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label :for="`dlf-${id}`" class="label is-unselectable">
|
<label :for="`dlf-${id}`" class="label is-unselectable">
|
||||||
<span v-if="icon" class="icon"><i :class="icon" /></span>
|
<template v-if="$slots.title">
|
||||||
<span v-tooltip="field ? `yt-dlp option: ${field}` : null" :class="{ 'has-tooltip': field }">{{ label }}</span>
|
<slot name="title"></slot>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span v-if="icon" class="icon"><i :class="icon" /></span>
|
||||||
|
<span v-tooltip="field ? `yt-dlp option: ${field}` : null" :class="{ 'has-tooltip': field }">{{ label }}</span>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div class="control is-expanded" v-if="'string' === type">
|
<div class="control is-expanded" v-if="'string' === type">
|
||||||
<input :id="`dlf-${id}`" :type="type" class="input" v-model="model" :placeholder="placeholder"
|
<input :id="`dlf-${id}`" :type="type" class="input" v-model="model" :placeholder="placeholder"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
|
<style>
|
||||||
|
.model-content {
|
||||||
|
width: 70vw;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="modal is-active">
|
<div class="modal is-active">
|
||||||
<div class="model-title" v-if="title" />
|
<div class="model-title" v-if="title" />
|
||||||
<div class="modal-background" @click="emitter('close')"></div>
|
<div class="modal-background" @click="emitter('close')"></div>
|
||||||
<div class="modal-content" style="width:70vw;">
|
<div class="modal-content" :class="contentClass">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="emitter('close')"></button>
|
<button class="modal-close is-large" aria-label="close" @click="emitter('close')"></button>
|
||||||
|
|
@ -22,6 +27,11 @@ defineProps({
|
||||||
default: '',
|
default: '',
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
contentClass: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handle_event = (e: KeyboardEvent) => {
|
const handle_event = (e: KeyboardEvent) => {
|
||||||
|
|
|
||||||
|
|
@ -118,16 +118,18 @@
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<DLInput id="cli_options" type="text" label="Command options for yt-dlp" v-model="form.cli"
|
<DLInput id="cli_options" type="text" label="Command options for yt-dlp" v-model="form.cli"
|
||||||
icon="fa-solid fa-terminal" :disabled="!socket.isConnected || addInProgress">
|
icon="fa-solid fa-terminal" :disabled="!socket.isConnected || addInProgress">
|
||||||
|
<template #title>
|
||||||
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
|
<span>Command options for yt-dlp -
|
||||||
|
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
<template #help>
|
<template #help>
|
||||||
<span class="help is-bold">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Check <NuxtLink target="_blank" to="https://github.com/yt-dlp/yt-dlp#general-options">this
|
<span>Not all options are supported <NuxtLink target="_blank"
|
||||||
page</NuxtLink> for more info. <span class="has-text-danger">Not all options are supported
|
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
|
||||||
<NuxtLink target="_blank"
|
ignored</NuxtLink>. Use with caution.</span>
|
||||||
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>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</DLInput>
|
</DLInput>
|
||||||
|
|
@ -230,6 +232,9 @@
|
||||||
@cancel="() => dialog_confirm.visible = false" />
|
@cancel="() => dialog_confirm.visible = false" />
|
||||||
|
|
||||||
<DLFields v-if="showFields" @cancel="() => showFields = false" />
|
<DLFields v-if="showFields" @cancel="() => showFields = false" />
|
||||||
|
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
||||||
|
<YTDLPOptions />
|
||||||
|
</Modal>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -252,10 +257,11 @@ 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)
|
||||||
|
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
||||||
|
|
||||||
const addInProgress = ref<boolean>(false)
|
const addInProgress = ref<boolean>(false)
|
||||||
const showFields = ref<boolean>(false)
|
const showFields = ref<boolean>(false)
|
||||||
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
const showOptions = ref<boolean>(false)
|
||||||
const dlFieldsExtra = ['--no-download-archive']
|
const dlFieldsExtra = ['--no-download-archive']
|
||||||
|
|
||||||
const form = useStorage<item_request>('local_config_v1', {
|
const form = useStorage<item_request>('local_config_v1', {
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
|
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>The name to refers to this preset of settings.</span>
|
<span>The name to refers to this preset of settings.</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
|
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
|
||||||
v-model="form.folder" :disabled="addInProgress" list="folders">
|
v-model="form.folder" :disabled="addInProgress" list="folders">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Use this folder if non is given with URL. Leave empty to use default download path. Default
|
<span>Use this folder if non is given with URL. Leave empty to use default download path. Default
|
||||||
download path <code>{{ config.app.download_path }}</code>.</span>
|
download path <code>{{ config.app.download_path }}</code>.</span>
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
<input type="text" class="input" id="output_template" :disabled="addInProgress"
|
<input type="text" class="input" id="output_template" :disabled="addInProgress"
|
||||||
placeholder="Leave empty to use default template." v-model="form.template">
|
placeholder="Leave empty to use default template." v-model="form.template">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Use this output template if non are given with URL. if not set, it will defaults to
|
<span>Use this output template if non are given with URL. if not set, it will defaults to
|
||||||
<code>{{ config.app.output_template }}</code>.
|
<code>{{ config.app.output_template }}</code>.
|
||||||
|
|
@ -146,20 +146,19 @@
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="cli_options">
|
<label class="label is-inline" for="cli_options">
|
||||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
<span>Command options for yt-dlp -
|
||||||
Command options for yt-dlp
|
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
||||||
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail" />
|
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>yt-dlp cli arguments. Check <NuxtLink target="_blank"
|
<span>Not all options are supported <NuxtLink target="_blank"
|
||||||
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
|
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
|
||||||
For more info. <span class="has-text-danger">Not all options are supported some are ignored. Use
|
ignored</NuxtLink>. Use with caution.</span>
|
||||||
with caution those arguments can break yt-dlp or the frontend.</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,7 +173,7 @@
|
||||||
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress"
|
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress"
|
||||||
placeholder="Leave empty to use default cookies" />
|
placeholder="Leave empty to use default cookies" />
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Use this cookies if non are given with the URL. Use the <NuxtLink target="_blank"
|
<span>Use this cookies if non are given with the URL. Use the <NuxtLink target="_blank"
|
||||||
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
||||||
|
|
@ -195,7 +194,7 @@
|
||||||
<textarea class="textarea" id="description" v-model="form.description" :disabled="addInProgress"
|
<textarea class="textarea" id="description" v-model="form.description" :disabled="addInProgress"
|
||||||
placeholder="Extras instructions for users to follow" />
|
placeholder="Extras instructions for users to follow" />
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Use this field to help users to understand how to use this preset.</span>
|
<span>Use this field to help users to understand how to use this preset.</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -227,6 +226,10 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
||||||
|
<YTDLPOptions />
|
||||||
|
</Modal>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -252,6 +255,7 @@ const form = reactive<Preset>(JSON.parse(JSON.stringify(props.preset)))
|
||||||
const import_string = ref<string>('')
|
const import_string = ref<string>('')
|
||||||
const showImport = useStorage<boolean>('showImport', false)
|
const showImport = useStorage<boolean>('showImport', false)
|
||||||
const selected_preset = ref<string>('')
|
const selected_preset = ref<string>('')
|
||||||
|
const showOptions = ref<boolean>(false)
|
||||||
|
|
||||||
const checkInfo = async (): Promise<void> => {
|
const checkInfo = async (): Promise<void> => {
|
||||||
for (const key of ['name']) {
|
for (const key of ['name']) {
|
||||||
|
|
|
||||||
|
|
@ -234,20 +234,20 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="cli_options">
|
<label class="label is-inline" for="cli_options">
|
||||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
Command options for yt-dlp
|
<span>Command options for yt-dlp -
|
||||||
|
<NuxtLink @click="showOptions = true" v-text="'View Options'" />
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea type="text" class="textarea is-pre" v-model="form.cli" id="cli_options"
|
<textarea type="text" class="textarea is-pre" v-model="form.cli" id="cli_options"
|
||||||
:disabled="addInProgress"
|
:disabled="addInProgress"
|
||||||
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail" />
|
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help is-bold">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span class="is-bold">yt-dlp cli arguments. Check <NuxtLink target="_blank"
|
<span>Not all options are supported <NuxtLink target="_blank"
|
||||||
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
|
to="https://github.com/arabcoders/ytptube/blob/master/app/library/Utils.py#L26">some are
|
||||||
For more info. <span class="has-text-danger">Not all options are supported some are ignored. Use
|
ignored</NuxtLink>. Use with caution.</span>
|
||||||
with caution those arguments can break yt-dlp or the frontend.</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -297,6 +297,9 @@
|
||||||
<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>
|
||||||
|
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
||||||
|
<YTDLPOptions />
|
||||||
|
</Modal>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -324,6 +327,7 @@ const showImport = useStorage('showImport', false)
|
||||||
|
|
||||||
const convertInProgress = ref<boolean>(false)
|
const convertInProgress = ref<boolean>(false)
|
||||||
const import_string = ref<string>('')
|
const import_string = ref<string>('')
|
||||||
|
const showOptions = ref<boolean>(false)
|
||||||
|
|
||||||
const CHANNEL_REGEX = /^https?:\/\/(?:www\.)?youtube\.com\/(?:(?:channel\/(?<channelId>UC[0-9A-Za-z_-]{22}))|(?:c\/(?<customName>[A-Za-z0-9_-]+))|(?:user\/(?<userName>[A-Za-z0-9_-]+))|(?:@(?<handle>[A-Za-z0-9_-]+)))(?<suffix>\/.*)?\/?$/
|
const CHANNEL_REGEX = /^https?:\/\/(?:www\.)?youtube\.com\/(?:(?:channel\/(?<channelId>UC[0-9A-Za-z_-]{22}))|(?:c\/(?<customName>[A-Za-z0-9_-]+))|(?:user\/(?<userName>[A-Za-z0-9_-]+))|(?:@(?<handle>[A-Za-z0-9_-]+)))(?<suffix>\/.*)?\/?$/
|
||||||
|
|
||||||
|
|
|
||||||
285
ui/app/components/YTDLPOptions.vue
Normal file
285
ui/app/components/YTDLPOptions.vue
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
<!-- ui/app/pages/options.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="p-1 container" style="border-radius: 0; padding:0">
|
||||||
|
|
||||||
|
<div class="box m-2">
|
||||||
|
<div class="columns is-multiline is-vcentered">
|
||||||
|
<div class="column is-12 is-6-desktop">
|
||||||
|
<label class="label is-small">Search</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input v-model.trim="filters.query" type="text" class="input" placeholder="Filter by flag or description..."
|
||||||
|
autocomplete="off" />
|
||||||
|
<span class="icon is-left"><i class="fa-solid fa-magnifying-glass" /></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-3-desktop">
|
||||||
|
<label class="label is-small">Group Filter</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select v-model="filters.group">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option v-for="g in groupNames" :key="g" :value="g">{{ g }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-3-desktop">
|
||||||
|
<label class="label is-small">Display</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select v-model="displayMode">
|
||||||
|
<option value="grouped">Grouped</option>
|
||||||
|
<option value="list">List</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-3-desktop">
|
||||||
|
<label class="label is-small">Sort By</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select v-model="sortBy">
|
||||||
|
<option value="flag">Flag</option>
|
||||||
|
<option value="group">Group</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-3-desktop">
|
||||||
|
<label class="label is-small">Order</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select v-model="sortDir">
|
||||||
|
<option value="asc">Asc</option>
|
||||||
|
<option value="desc">Desc</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12 is-6-desktop">
|
||||||
|
<label class="label is-small">Flags</label>
|
||||||
|
<div class="buttons are-small">
|
||||||
|
<button class="button" :class="{ 'is-link': filters.flagKind === 'any' }"
|
||||||
|
@click="filters.flagKind = 'any'">Any</button>
|
||||||
|
<button class="button" :class="{ 'is-link': filters.flagKind === 'short' }"
|
||||||
|
@click="filters.flagKind = 'short'">Short Only (-x)</button>
|
||||||
|
<button class="button" :class="{ 'is-link': filters.flagKind === 'long' }"
|
||||||
|
@click="filters.flagKind = 'long'">Long Only (--xyz)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="0 === visible.length" class="has-text-centered has-text-grey">
|
||||||
|
<p>No options match your criteria.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="'grouped' === displayMode && 0 !== grouped.length">
|
||||||
|
<section v-for="group in grouped" :key="group.name" class="m-2 mb-5">
|
||||||
|
<h2 class="title is-6 mb-3">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fa-regular fa-folder-open" /></span>
|
||||||
|
<span>{{ group.name }} <small class="has-text-grey">({{ group.items.length }})</small></span>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 30%">Flags</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="opt in group.items" :key="opt.flags.join('|')">
|
||||||
|
<tr v-if="!opt.ignored">
|
||||||
|
<td>
|
||||||
|
<i class="has-text-primary is-pointer is-pulled-right fa-regular fa-copy is-unselectable"
|
||||||
|
@click="copyFlag(opt.flags)" />
|
||||||
|
<div class="is-flex is-align-items-center">
|
||||||
|
<div class="tags">
|
||||||
|
<span v-for="f in opt.flags" :key="f" class="tag is-info">{{ f }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="opt.description && 0 !== opt.description.length">{{ opt.description }}</span>
|
||||||
|
<span v-else class="has-text-grey">—</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-bordered m-2">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 30%">Flags</th>
|
||||||
|
<th style="width: 20%">Group</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="opt in visible" :key="opt.flags.join('|')">
|
||||||
|
<tr v-if="!opt.ignored">
|
||||||
|
<td>
|
||||||
|
<i class="has-text-primary is-pointer is-pulled-right fa-regular fa-copy is-unselectable"
|
||||||
|
@click="copyFlag(opt.flags)" />
|
||||||
|
<div class="is-flex is-align-items-center">
|
||||||
|
<div class="tags">
|
||||||
|
<span v-for="f in opt.flags" :key="f" class="tag is-info">{{ f }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="is-bold">
|
||||||
|
{{ opt.group || 'root' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="opt.description && 0 !== opt.description.length">{{ opt.description }}</span>
|
||||||
|
<span v-else class="has-text-grey">—</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
|
type YTDLPOption = {
|
||||||
|
flags: string[],
|
||||||
|
description: string | null,
|
||||||
|
group: string | null,
|
||||||
|
ignored: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const options = ref<YTDLPOption[]>([])
|
||||||
|
const displayMode = useStorage<'grouped' | 'list'>('opts_display', 'grouped')
|
||||||
|
const sortBy = useStorage<'flag' | 'group'>('opts_sort_by', 'flag')
|
||||||
|
const sortDir = useStorage<'asc' | 'desc'>('opts_sort_dir', 'asc')
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
query: '',
|
||||||
|
group: '',
|
||||||
|
flagKind: 'any' as 'any' | 'short' | 'long',
|
||||||
|
})
|
||||||
|
|
||||||
|
const reload = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
const resp = await request('/api/yt-dlp/options', { credentials: 'include' })
|
||||||
|
if (!resp.ok) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = await resp.json()
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
options.value = data as YTDLPOption[]
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyFlag = async (flags: string[]): Promise<void> => {
|
||||||
|
const longFlag = flags.find(f => f.startsWith('--'))
|
||||||
|
if (!longFlag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copyText(longFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => await reload())
|
||||||
|
|
||||||
|
const groupNames = computed<string[]>(() => {
|
||||||
|
const s = new Set<string>()
|
||||||
|
for (const o of options.value) {
|
||||||
|
s.add(o.group || 'root')
|
||||||
|
}
|
||||||
|
return Array.from(s).sort((a, b) => a.localeCompare(b))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filtered = computed<YTDLPOption[]>(() => {
|
||||||
|
const q = filters.query.toLowerCase()
|
||||||
|
const g = filters.group
|
||||||
|
|
||||||
|
return options.value.filter((o) => {
|
||||||
|
if (g && (o.group || 'root') !== g) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('short' === filters.flagKind && !o.flags.some((f) => /^-\w(,|$)|^-\w$/.test(f))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('long' === filters.flagKind && !o.flags.some((f) => /^--[a-zA-Z0-9][\w-]*/.test(f))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 !== q.length) {
|
||||||
|
const hay = [o.flags.join(' '), o.description || '', o.group || 'root'].join(' ').toLowerCase()
|
||||||
|
if (-1 === hay.indexOf(q)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const sorted = computed<YTDLPOption[]>(() => {
|
||||||
|
const dir = 'asc' === sortDir.value ? 1 : -1
|
||||||
|
const arr = [...filtered.value]
|
||||||
|
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
if ('group' === sortBy.value) {
|
||||||
|
const ga = (a.group || 'root').localeCompare(b.group || 'root')
|
||||||
|
if (0 !== ga) {
|
||||||
|
return ga * dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fa = (a.flags[0] || '').localeCompare(b.flags[0] || '')
|
||||||
|
return fa * dir
|
||||||
|
})
|
||||||
|
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = computed<YTDLPOption[]>(() => sorted.value)
|
||||||
|
|
||||||
|
const grouped = computed<{ name: string, items: YTDLPOption[] }[]>(() => {
|
||||||
|
const map = new Map<string, YTDLPOption[]>()
|
||||||
|
|
||||||
|
for (const o of visible.value) {
|
||||||
|
const key = o.group || 'root'
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, [])
|
||||||
|
}
|
||||||
|
map.get(key)!.push(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = Array.from(map.entries()).map(([name, items]) => {
|
||||||
|
items.sort((a, b) => (a.flags[0] || '').localeCompare(b.flags[0] || ''))
|
||||||
|
return { name, items }
|
||||||
|
})
|
||||||
|
|
||||||
|
out.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table td,
|
||||||
|
.table th {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
ui/app/composables/useMediaQuery.ts
Normal file
61
ui/app/composables/useMediaQuery.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { ref, onMounted, onBeforeUnmount, shallowReadonly, type Ref } from 'vue'
|
||||||
|
|
||||||
|
export interface MediaQueryOptions {
|
||||||
|
/**
|
||||||
|
* A custom media query string.
|
||||||
|
* Example: "(max-width: 640px) or (hover: none)"
|
||||||
|
* If provided, takes precedence over maxWidth.
|
||||||
|
*/
|
||||||
|
query?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max width in px considered true.
|
||||||
|
* Ignored if `query` is provided. Default: 768.
|
||||||
|
*/
|
||||||
|
maxWidth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive state of a CSS media query.
|
||||||
|
*/
|
||||||
|
export function useMediaQuery(options: MediaQueryOptions = {}): Readonly<Ref<boolean>> {
|
||||||
|
const query = options.query ?? `(max-width: ${options.maxWidth ?? 1024}px)`
|
||||||
|
const matches = ref(false)
|
||||||
|
|
||||||
|
let mql: MediaQueryList | null = null
|
||||||
|
let onChange: ((ev: MediaQueryListEvent) => void) | null = null
|
||||||
|
|
||||||
|
const setup = () => {
|
||||||
|
if ('undefined' === typeof window || !window.matchMedia) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mql = window.matchMedia(query)
|
||||||
|
matches.value = mql.matches
|
||||||
|
|
||||||
|
onChange = ev => { matches.value = ev.matches }
|
||||||
|
if ('addEventListener' in mql) {
|
||||||
|
mql.addEventListener('change', onChange as EventListener)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error legacy Safari
|
||||||
|
mql.addListener(onChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
const teardown = () => {
|
||||||
|
if (!mql || !onChange) return
|
||||||
|
if ('removeEventListener' in mql) {
|
||||||
|
mql.removeEventListener('change', onChange as EventListener)
|
||||||
|
} else {
|
||||||
|
// @ts-expect-error legacy Safari
|
||||||
|
mql.removeListener(onChange)
|
||||||
|
}
|
||||||
|
mql = null
|
||||||
|
onChange = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(setup)
|
||||||
|
onBeforeUnmount(teardown)
|
||||||
|
|
||||||
|
return shallowReadonly(matches)
|
||||||
|
}
|
||||||
|
|
@ -75,27 +75,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item is-hidden-mobile" v-if="false === config.app.is_native">
|
<div class="navbar-item" v-if="false === config.app.is_native">
|
||||||
<button class="button is-dark" @click="reloadPage">
|
<button class="button is-dark" @click="reloadPage">
|
||||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||||
</button>
|
<span v-if="isMobile">Reload</span>
|
||||||
</div>
|
|
||||||
<div class="navbar-item is-hidden-tablet">
|
|
||||||
<button class="button is-dark" @click="reloadPage">
|
|
||||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
|
||||||
<span>Reload</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NotifyDropdown />
|
<NotifyDropdown />
|
||||||
|
|
||||||
<div class="navbar-item is-hidden-mobile">
|
<div class="navbar-item" v-if="!isMobile">
|
||||||
<button class="button is-dark has-tooltip-bottom mr-4" v-tooltip.bottom="'WebUI Settings'"
|
<button class="button is-dark has-tooltip-bottom mr-4" v-tooltip.bottom="'WebUI Settings'"
|
||||||
@click="show_settings = !show_settings">
|
@click="show_settings = !show_settings">
|
||||||
<span class="icon"><i class="fas fa-cog" /></span>
|
<span class="icon"><i class="fas fa-cog" /></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item is-hidden-tablet">
|
<div class="navbar-item" v-if="isMobile">
|
||||||
<button class="button is-dark" @click="show_settings = !show_settings">
|
<button class="button is-dark" @click="show_settings = !show_settings">
|
||||||
<span class="icon"><i class="fas fa-cog" /></span>
|
<span class="icon"><i class="fas fa-cog" /></span>
|
||||||
<span>WebUI Settings</span>
|
<span>WebUI Settings</span>
|
||||||
|
|
@ -116,11 +111,11 @@
|
||||||
<div class="column is-8-mobile">
|
<div class="column is-8-mobile">
|
||||||
<div class="has-text-left" v-if="config.app?.app_version">
|
<div class="has-text-left" v-if="config.app?.app_version">
|
||||||
© {{ Year }} - <NuxtLink href="https://github.com/ArabCoders/ytptube" target="_blank">YTPTube</NuxtLink>
|
© {{ Year }} - <NuxtLink href="https://github.com/ArabCoders/ytptube" target="_blank">YTPTube</NuxtLink>
|
||||||
<span class="is-hidden-mobile has-tooltip"
|
<span class="has-tooltip" v-if="!isMobile"
|
||||||
v-tooltip="`Build Date: ${config.app?.app_build_date}, Branch: ${config.app?.app_branch}, commit: ${config.app?.app_commit_sha}`">
|
v-tooltip="`Build Date: ${config.app?.app_build_date}, Branch: ${config.app?.app_branch}, commit: ${config.app?.app_commit_sha}`">
|
||||||
({{ config?.app?.app_version || 'unknown' }})</span>
|
({{ config?.app?.app_version || 'unknown' }})</span>
|
||||||
- <NuxtLink target="_blank" href="https://github.com/yt-dlp/yt-dlp">yt-dlp</NuxtLink>
|
- <NuxtLink target="_blank" href="https://github.com/yt-dlp/yt-dlp">yt-dlp</NuxtLink>
|
||||||
<span class="is-hidden-mobile"> ({{ config?.app?.ytdlp_version || 'unknown' }})</span>
|
<span v-if="!isMobile"> ({{ config?.app?.ytdlp_version || 'unknown' }})</span>
|
||||||
- <NuxtLink to="/changelog">CHANGELOG</NuxtLink>
|
- <NuxtLink to="/changelog">CHANGELOG</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,6 +132,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch, readonly } from 'vue'
|
||||||
import 'assets/css/bulma.css'
|
import 'assets/css/bulma.css'
|
||||||
import 'assets/css/style.css'
|
import 'assets/css/style.css'
|
||||||
import 'assets/css/all.css'
|
import 'assets/css/all.css'
|
||||||
|
|
@ -154,6 +150,7 @@ const loadingImage = ref(false)
|
||||||
const bg_enable = useStorage('random_bg', true)
|
const bg_enable = useStorage('random_bg', true)
|
||||||
const bg_opacity = useStorage('random_bg_opacity', 0.95)
|
const bg_opacity = useStorage('random_bg_opacity', 0.95)
|
||||||
const showMenu = ref(false)
|
const showMenu = ref(false)
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 1024 })
|
||||||
|
|
||||||
const applyPreferredColorScheme = (scheme: string) => {
|
const applyPreferredColorScheme = (scheme: string) => {
|
||||||
if (!scheme || scheme === 'auto') {
|
if (!scheme || scheme === 'auto') {
|
||||||
|
|
|
||||||
|
|
@ -20,26 +20,26 @@
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
|
<button class="button is-danger is-light" @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>
|
<span v-if="!isMobile">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">
|
||||||
<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>
|
<span v-if="!isMobile">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>
|
<span v-if="!isMobile">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 class="button is-primary has-tooltip-bottom" @click="config.showForm = !config.showForm">
|
<button class="button is-primary has-tooltip-bottom" @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>
|
<span v-if="!isMobile">New Download</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -48,13 +48,14 @@
|
||||||
@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>
|
<span v-if="!isMobile">
|
||||||
|
{{ display_style === 'cards' ? 'Cards' : 'List' }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-hidden-mobile">
|
<div v-if="!isMobile">
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
Queued and completed downloads are displayed here.
|
Queued and completed downloads are displayed here.
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -68,7 +69,7 @@
|
||||||
<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: item_request) => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail"
|
@add_new="(item: Partial<StoreItem>) => 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()" />
|
||||||
|
|
@ -81,6 +82,7 @@
|
||||||
<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'
|
import type { item_request } from '~/types/item'
|
||||||
|
import type { StoreItem } from '~/types/store'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const stateStore = useStateStore()
|
const stateStore = useStateStore()
|
||||||
|
|
@ -106,6 +108,8 @@ const dialog_confirm = ref({
|
||||||
options: [],
|
options: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 1024 })
|
||||||
|
|
||||||
watch(toggleFilter, () => {
|
watch(toggleFilter, () => {
|
||||||
if (!toggleFilter.value) {
|
if (!toggleFilter.value) {
|
||||||
query.value = ''
|
query.value = ''
|
||||||
|
|
@ -170,7 +174,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: item_request) => {
|
const toNewDownload = async (item: item_request | Partial<StoreItem>) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue