refactor: minor UX fixes.

This commit is contained in:
arabcoders 2026-01-29 16:48:18 +03:00
parent 651aa14882
commit ed6cdf1f92
7 changed files with 64 additions and 111 deletions

18
FAQ.md
View file

@ -317,22 +317,13 @@ Then simply create a new preset, and in the `Command options for yt-dlp` field s
```bash ```bash
--extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416" --extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416"
--extractor-args "youtube:player-client=default,tv,mweb;formats=incomplete"
``` ```
you and also enable the fallback by using the follow extractor args
```bash
--extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416;disable_innertube=1"
--extractor-args "youtube:player-client=default,tv,mweb;formats=incomplete"
```
Use this alternative extractor args in case the extractor fails to get the pot tokens from the bgutil provider server.
For more information please visit [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider) project. For more information please visit [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider) project.
# Troubleshooting and submitting issues # Troubleshooting and submitting issues
Before asking a question or submitting an issue for YTPTube, please remember that YTPTube is only a thin wrapper for Before asking a question or submitting an issue for YTPTube, please remember that YTPTube is only a wrapper for
[yt-dlp](https://github.com/yt-dlp/yt-dlp). Any issues you might be experiencing with authentication to video websites, [yt-dlp](https://github.com/yt-dlp/yt-dlp). Any issues you might be experiencing with authentication to video websites,
postprocessing, permissions, other `yt-dlp options` configurations which seem not to work, or anything else that postprocessing, permissions, other `yt-dlp options` configurations which seem not to work, or anything else that
concerns the workings of the underlying yt-dlp library, need not be opened on the YTPTube project. concerns the workings of the underlying yt-dlp library, need not be opened on the YTPTube project.
@ -343,7 +334,8 @@ and once that is working, importing the options that worked for you into a new `
## Via HTTP ## Via HTTP
If you have enabled the web terminal via `YTP_CONSOLE_ENABLED` environment variable, simply go to `Other > Terminal` use If you have enabled the web terminal via `YTP_CONSOLE_ENABLED` environment variable, simply go to `Other > Terminal` use
the yt-dlp command, the interface is jailed to the `yt-dlp` binary you can't access anything else. the yt-dlp command, the interface is jailed to the `yt-dlp` binary you can't access anything else. Or from download form
by clicking `advanced options` button than the yellow terminal icon `Run directly in console`.
## Via CLI ## Via CLI
@ -503,7 +495,7 @@ as we only test for GPU encoding once on first video stream.
# Allowing internal URLs requests # Allowing internal URLs requests
By default, YTPTube prevents requests to internal resources, for security reasons. However, if you want to allow requests to internal URLs, you can set the `YTP_ALLOW_INTERNAL_URLS` environment variable to `true`. This will allow requests to internal URLs. By default, YTPTube prevents requests to internal resources. However, if you want to allow requests to internal URLs, you can set the `YTP_ALLOW_INTERNAL_URLS` environment variable to `true`. This will allow requests to internal URLs.
We do not recommend enabling this option unless you know what you are doing, as it can expose your internal network to We do not recommend enabling this option unless you know what you are doing, as it can expose your internal network to
potential security risks. This should only be used if it's truly needed. potential security risks. This should only be used if it's truly needed.
@ -557,7 +549,6 @@ services:
After making the changes, restart your container. This should resolve the "No space left on device" After making the changes, restart your container. This should resolve the "No space left on device"
error during download. error during download.
# How to prevent loading screen during YouTube premieres? # How to prevent loading screen during YouTube premieres?
Depending on how you look at it, YTPTube live download implementation is rather great and fast. However, during YouTube Depending on how you look at it, YTPTube live download implementation is rather great and fast. However, during YouTube
@ -566,7 +557,6 @@ playing. By default we wait for 5min + the duration of the video before starting
the loading screen. However, you can override the behavior by setting the following environment variable: the loading screen. However, you can override the behavior by setting the following environment variable:
```env ```env
YTP_PREVENT_LIVE_PREMIERE=true
YTP_LIVE_PREMIERE_BUFFER=10 YTP_LIVE_PREMIERE_BUFFER=10
``` ```

View file

@ -201,7 +201,7 @@ class Config(metaclass=Singleton):
is_native: bool = False is_native: bool = False
"Is the application running in natively." "Is the application running in natively."
prevent_live_premiere: bool = False prevent_live_premiere: bool = True
"""Prevent downloading of the initial premiere live broadcast.""" """Prevent downloading of the initial premiere live broadcast."""
live_premiere_buffer: int = 5 live_premiere_buffer: int = 5

View file

@ -499,10 +499,11 @@ import { useStorage, useIntersectionObserver } from '@vueuse/core'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'
import { useConfirm } from '~/composables/useConfirm' import { useConfirm } from '~/composables/useConfirm'
import { deepIncludes, getPath, getImage } from '~/utils' import { deepIncludes, getPath, getImage } from '~/utils'
import type { item_request } from '~/types/item'
const emitter = defineEmits<{ const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void (e: 'getInfo', url: string, preset: string, cli: string): void
(e: 'add_new', item: Partial<StoreItem>): void (e: 'add_new', item: item_request): void
(e: 'getItemInfo', id: string): void (e: 'getItemInfo', id: string): void
(e: 'clear_search'): void (e: 'clear_search'): void
}>() }>()
@ -850,7 +851,7 @@ const removeItem = async (item: StoreItem) => {
} }
const retryItem = async (item: StoreItem, re_add: boolean = false, remove_file: boolean = false) => { const retryItem = async (item: StoreItem, re_add: boolean = false, remove_file: boolean = false) => {
const item_req: Partial<StoreItem> = { const item_req: item_request = {
url: item.url, url: item.url,
preset: item.preset, preset: item.preset,
folder: item.folder, folder: item.folder,

View file

@ -289,7 +289,7 @@
<span>Pause</span> <span>Pause</span>
</button> </button>
</div> </div>
<div class="column is-half-mobile"> <div class="column">
<Dropdown icons="fa-solid fa-cogs" label="Actions"> <Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="isEmbedable(item.url)"> <template v-if="isEmbedable(item.url)">
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url) as string"> <NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url) as string">

View file

@ -93,7 +93,7 @@
<div v-show="'history' === activeTab"> <div v-show="'history' === activeTab">
<History @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)" <History @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)"
@add_new="(item: Partial<StoreItem>) => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail" @add_new="(item: item_request) => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail"
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" /> @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
</div> </div>
</div> </div>

View file

@ -90,13 +90,13 @@
<div class="column is-2-tablet is-5-mobile"> <div class="column is-2-tablet is-5-mobile">
<Dropdown label="Actions" icons="fa-solid fa-list"> <Dropdown label="Actions" icons="fa-solid fa-list">
<a class="dropdown-item has-text-purple" @click="(selectedElms.length > 0 && !massRun) ? runConfirm() : null" <a class="dropdown-item has-text-purple" @click="(selectedElms.length > 0 && !massRun) ? runSelected() : null"
:style="{ opacity: (selectedElms.length < 1 || massRun) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massRun) ? 'not-allowed' : 'pointer' }"> :style="{ opacity: (selectedElms.length < 1 || massRun) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massRun) ? 'not-allowed' : 'pointer' }">
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span> <span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
<span>Run Selected</span> <span>Run Selected</span>
</a> </a>
<a class="dropdown-item has-text-danger" <a class="dropdown-item has-text-danger"
@click="(selectedElms.length > 0 && !massDelete) ? deleteConfirm() : null" @click="(selectedElms.length > 0 && !massDelete) ? deleteSelected() : null"
:style="{ opacity: (selectedElms.length < 1 || massDelete) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massDelete) ? 'not-allowed' : 'pointer' }"> :style="{ opacity: (selectedElms.length < 1 || massDelete) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massDelete) ? 'not-allowed' : 'pointer' }">
<span class="icon"><i class="fa-solid fa-trash-can" /></span> <span class="icon"><i class="fa-solid fa-trash-can" /></span>
<span>Remove Selected</span> <span>Remove Selected</span>
@ -448,10 +448,6 @@
</div> </div>
</div> </div>
<ConfirmDialog v-if="dialog_confirm.visible" :visible="dialog_confirm.visible" :title="dialog_confirm.title"
:message="dialog_confirm.message" :options="dialog_confirm.options" @confirm="dialog_confirm.confirm"
:html_message="dialog_confirm.html_message" @cancel="dialog_confirm = reset_dialog()" />
<Modal v-if="inspectTask" @close="() => inspectTask = null" :contentClass="`modal-content-max`"> <Modal v-if="inspectTask" @close="() => inspectTask = null" :contentClass="`modal-content-max`">
<TaskInspect :url="inspectTask.url" :preset="inspectTask.preset" /> <TaskInspect :url="inspectTask.url" :preset="inspectTask.preset" />
</Modal> </Modal>
@ -470,6 +466,7 @@ import type { Task, ExportedTask } from '~/types/tasks'
import type { WSEP } from '~/types/sockets' import type { WSEP } from '~/types/sockets'
import { sleep } from '~/utils' import { sleep } from '~/utils'
import { useSessionCache } from '~/utils/cache' import { useSessionCache } from '~/utils/cache'
import type { item_request } from '~/types/item'
type TaskWithUI = Task & { in_progress?: boolean } type TaskWithUI = Task & { in_progress?: boolean }
@ -478,16 +475,15 @@ const toast = useNotification()
const config = useConfigStore() const config = useConfigStore()
const socket = useSocketStore() const socket = useSocketStore()
const stateStore = useStateStore() const stateStore = useStateStore()
const { confirmDialog: cDialog } = useDialog() const { confirmDialog } = useDialog()
const sessionCache = useSessionCache() const sessionCache = useSessionCache()
const display_style = useStorage<string>("tasks_display_style", "cards") const display_style = useStorage<string>("tasks_display_style", "cards")
const isMobile = useMediaQuery({ maxWidth: 1024 }) const isMobile = useMediaQuery({ maxWidth: 1024 })
// Use the tasks composable
const tasksComposable = useTasks() const tasksComposable = useTasks()
const { tasks, isLoading, addInProgress } = tasksComposable const { tasks, isLoading, addInProgress } = tasksComposable
const task = ref<TaskWithUI | Record<string, unknown>>({}) const task = ref<Partial<TaskWithUI>>({})
const taskRef = ref<number | null>(null) const taskRef = ref<number | null>(null)
const toggleForm = ref<boolean>(false) const toggleForm = ref<boolean>(false)
const selectedElms = ref<Array<number>>([]) const selectedElms = ref<Array<number>>([])
@ -496,20 +492,10 @@ const massRun = ref<boolean>(false)
const massDelete = ref<boolean>(false) const massDelete = ref<boolean>(false)
const table_container = ref(false) const table_container = ref(false)
const inspectTask = ref<TaskWithUI | null>(null) const inspectTask = ref<TaskWithUI | null>(null)
const reset_dialog = () => ({
visible: false,
title: 'Confirm Action',
confirm: (_opts: any) => { },
message: '',
html_message: '',
options: [],
});
const dialog_confirm = ref(reset_dialog())
const query = ref() const query = ref()
const toggleFilter = ref(false) const toggleFilter = ref(false)
const CACHE_KEY = 'tasks:handler_support'
const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {})
watch(toggleFilter, () => { watch(toggleFilter, () => {
if (!toggleFilter.value) { if (!toggleFilter.value) {
@ -522,18 +508,19 @@ watch(query, () => {
selectedElms.value = [] selectedElms.value = []
}) })
watch(masterSelectAll, value => { watch(masterSelectAll, value => {
if (!value) { if (!value) {
selectedElms.value = [] selectedElms.value = []
return return
} }
for (const key in filteredTasks.value) { for (const key in filteredTasks.value) {
const element = filteredTasks.value[key] as Task const element = filteredTasks.value[key] as Task
if (element.id) { if (element.id) {
selectedElms.value.push(element.id) selectedElms.value.push(element.id)
} }
} }
}) })
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
@ -543,10 +530,7 @@ watch(() => socket.isConnected, async () => {
socket.on('item_status', statusHandler) socket.on('item_status', statusHandler)
}) })
watch(taskHandlerSupport, newValue => sessionCache.set(CACHE_KEY, newValue), { deep: true })
const CACHE_KEY = 'tasks:handler_support'
const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {})
watch(taskHandlerSupport, (newValue) => sessionCache.set(CACHE_KEY, newValue), { deep: true })
const getCacheKey = (task: Task): string => `${task.id}:${task.url}` const getCacheKey = (task: Task): string => `${task.id}:${task.url}`
@ -581,7 +565,7 @@ const checkHandlerSupport = async (task: Task): Promise<boolean> => {
url: task.url, url: task.url,
static_only: true static_only: true
}) })
const supported = result?.matched === true const supported = true === result?.matched
taskHandlerSupport.value[cacheKey] = supported taskHandlerSupport.value[cacheKey] = supported
return supported return supported
} catch { } catch {
@ -624,7 +608,6 @@ const reloadContent = async (fromMounted: boolean = false) => {
} }
} }
const resetForm = (closeForm: boolean = false) => { const resetForm = (closeForm: boolean = false) => {
task.value = { task.value = {
name: '', name: '',
@ -644,38 +627,39 @@ const resetForm = (closeForm: boolean = false) => {
} }
} }
const deleteConfirm = () => {
if (selectedElms.value.length < 1) {
toast.error('No tasks selected.')
return
}
dialog_confirm.value.visible = true
dialog_confirm.value.title = 'Delete Selected Tasks'
dialog_confirm.value.message = `Delete ${selectedElms.value.length} task/s?`
dialog_confirm.value.confirm = async () => await deleteSelected()
}
const deleteSelected = async () => { const deleteSelected = async () => {
if (selectedElms.value.length < 1) { if (selectedElms.value.length < 1) {
toast.error('No tasks selected.') toast.error('No tasks selected.')
return return
} }
massDelete.value = true const { status } = await confirmDialog({
dialog_confirm.value = reset_dialog() title: 'Delete Selected Tasks',
rawHTML: `Delete <strong class="has-text-danger">${selectedElms.value.length}</strong> task/s?<ul>` + selectedElms.value.map(id => {
const item = tasks.value.find(t => t.id === id)
return item ? `<li>${item.id}: ${item.name}</li>` : ''
}).join('') + `</ul>`,
confirmText: 'Delete',
confirmColor: 'is-danger'
})
if (true !== status) {
return
}
const itemsToDelete = tasks.value.filter(t => t.id && selectedElms.value.includes(t.id)) const itemsToDelete = tasks.value.filter(t => t.id && selectedElms.value.includes(t.id))
if (itemsToDelete.length < 1) { if (itemsToDelete.length < 1) {
toast.error('No tasks found to delete.') toast.error('No tasks found to delete.')
massDelete.value = false
return return
} }
// Delete tasks sequentially massDelete.value = true
for (const item of itemsToDelete) { for (const item of itemsToDelete) {
if (item.id) { if (!item.id) {
await tasksComposable.deleteTask(item.id) continue
} }
await tasksComposable.deleteTask(item.id)
} }
selectedElms.value = [] selectedElms.value = []
@ -687,15 +671,9 @@ const deleteSelected = async () => {
} }
const deleteItem = async (item: Task) => { const deleteItem = async (item: Task) => {
if (true !== (await box.confirm(`Delete '${item.name}' task?`))) { if (!item.id || true !== (await box.confirm(`Delete '${item.name}' task?`))) {
return return
} }
if (!item.id) {
toast.error('Task ID is missing')
return
}
await tasksComposable.deleteTask(item.id) await tasksComposable.deleteTask(item.id)
} }
@ -705,15 +683,9 @@ const toggleEnabled = async (item: Task) => {
return return
} }
const newStatus = !item.enabled const updated = await tasksComposable.patchTask(item.id, { enabled: !item.enabled })
const updated = await tasksComposable.patchTask(item.id, { enabled: newStatus })
if (updated) { if (updated) {
// Update local reference
item.enabled = updated.enabled item.enabled = updated.enabled
// Update handler support cache if needed
if (updated.enabled) { if (updated.enabled) {
await checkHandlerSupport(updated) await checkHandlerSupport(updated)
} }
@ -726,15 +698,9 @@ const toggleHandlerEnabled = async (item: Task) => {
return return
} }
const newStatus = !item.handler_enabled const updated = await tasksComposable.patchTask(item.id, { handler_enabled: !item.handler_enabled })
const updated = await tasksComposable.patchTask(item.id, { handler_enabled: newStatus })
if (updated) { if (updated) {
// Update local reference
item.handler_enabled = updated.handler_enabled item.handler_enabled = updated.handler_enabled
// Update handler support cache if needed
if (updated.handler_enabled) { if (updated.handler_enabled) {
await checkHandlerSupport(updated) await checkHandlerSupport(updated)
} }
@ -745,10 +711,8 @@ const updateItem = async ({ reference, task, archive_all }: { reference?: number
let createdOrUpdated: Task | null = null let createdOrUpdated: Task | null = null
if (reference) { if (reference) {
// Update existing task
createdOrUpdated = await tasksComposable.updateTask(reference, task) createdOrUpdated = await tasksComposable.updateTask(reference, task)
} else { } else {
// Create new task
createdOrUpdated = await tasksComposable.createTask(task) createdOrUpdated = await tasksComposable.createTask(task)
} }
@ -756,7 +720,6 @@ const updateItem = async ({ reference, task, archive_all }: { reference?: number
return return
} }
// Check handler support for the new/updated task
await checkHandlerSupport(createdOrUpdated) await checkHandlerSupport(createdOrUpdated)
if (!reference && true === archive_all && createdOrUpdated.id) { if (!reference && true === archive_all && createdOrUpdated.id) {
@ -799,29 +762,22 @@ const tryParse = (expression: string) => {
} }
} }
const runConfirm = () => {
if (selectedElms.value.length < 1) {
toast.error('No tasks selected.')
return
}
dialog_confirm.value.visible = true
dialog_confirm.value.title = 'Run Selected Tasks'
dialog_confirm.value.html_message = `Run the following tasks?<ul>` + selectedElms.value.map(id => {
const item = tasks.value.find(t => t.id === id)
return item ? `<li>${item.name}</li>` : ''
}).join('') + `</ul>`
dialog_confirm.value.confirm = async () => await runSelected()
}
const runSelected = async () => { const runSelected = async () => {
if (selectedElms.value.length < 1) { if (selectedElms.value.length < 1) {
toast.error('No tasks selected.') toast.error('No tasks selected.')
return return
} }
dialog_confirm.value = reset_dialog() const { status } = await confirmDialog({
rawHTML: `Run the following tasks?<ul>` + selectedElms.value.map(id => {
const item = tasks.value.find(t => t.id === id)
return item ? `<li>${item.name}</li>` : ''
}).join('') + `</ul>`
})
if (true !== status) {
return
}
massRun.value = true massRun.value = true
@ -851,9 +807,14 @@ const runNow = async (item: TaskWithUI, mass: boolean = false) => {
item.in_progress = true item.in_progress = true
} }
const data: Record<string, unknown> = { const data: item_request = {
url: item.url, url: item.url,
preset: item.preset, preset: item.preset,
extras: {
source_name: item.name,
source_id: item.id,
source_handler: "Web",
}
} }
if (item.folder) { if (item.folder) {
@ -868,7 +829,7 @@ const runNow = async (item: TaskWithUI, mass: boolean = false) => {
data.cli = item.cli data.cli = item.cli
} }
if (item?.auto_start !== undefined) { if (undefined !== item?.auto_start) {
data.auto_start = item.auto_start data.auto_start = item.auto_start
} }
@ -939,7 +900,7 @@ const archiveAll = async (item: TaskWithUI, by_pass: boolean = false) => {
try { try {
if (true !== by_pass) { if (true !== by_pass) {
const { status } = await cDialog({ const { status } = await confirmDialog({
message: `Mark all '${item.name}' items as downloaded in download archive?` message: `Mark all '${item.name}' items as downloaded in download archive?`
}) })
@ -965,7 +926,7 @@ const unarchiveAll = async (item: TaskWithUI) => {
} }
try { try {
const { status } = await cDialog({ const { status } = await confirmDialog({
message: `Remove all '${item.name}' items from download archive?` message: `Remove all '${item.name}' items from download archive?`
}) })
@ -990,7 +951,7 @@ const generateMeta = async (item: TaskWithUI) => {
} }
try { try {
const { status } = await cDialog({ const { status } = await confirmDialog({
rawHTML: ` rawHTML: `
<p> <p>
Generate '${item.name}' metadata? you will be notified when it is done. Generate '${item.name}' metadata? you will be notified when it is done.

View file

@ -1,4 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { item_request } from '~/types/item'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'
import { request } from '~/utils' import { request } from '~/utils'
@ -211,7 +212,7 @@ export const useStateStore = defineStore('state', () => {
* @param data - Download data (url, preset, folder, etc.) * @param data - Download data (url, preset, folder, etc.)
* @returns Promise that resolves when download is added * @returns Promise that resolves when download is added
*/ */
const addDownload = async (data: Record<string, unknown>): Promise<void> => { const addDownload = async (data: item_request): Promise<void> => {
const socket = useSocketStore() const socket = useSocketStore()
const toast = useNotification() const toast = useNotification()