minor design update

This commit is contained in:
arabcoders 2025-06-17 17:24:52 +03:00
parent 249bb43c15
commit 8694e82965
12 changed files with 122 additions and 88 deletions

View file

@ -76,6 +76,7 @@
"socketio", "socketio",
"timespec", "timespec",
"tmpfilename", "tmpfilename",
"ungroup",
"upgrader", "upgrader",
"urandom", "urandom",
"urlsafe", "urlsafe",

View file

@ -360,7 +360,7 @@ class DownloadQueue(metaclass=Singleton):
starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC) starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
) )
starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0)) starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0))
dlInfo.info.error += f" Download will start at {starts_in.isoformat()}." dlInfo.info.error += f" Download will start at {starts_in.astimezone().isoformat()}."
_requeue = False _requeue = False
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}") LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
@ -917,7 +917,7 @@ class DownloadQueue(metaclass=Singleton):
premiere_ends: datetime = starts_in + timedelta(minutes=5, seconds=duration) premiere_ends: datetime = starts_in + timedelta(minutes=5, seconds=duration)
if time_now < premiere_ends: if time_now < premiere_ends:
LOG.debug( LOG.debug(
f"Item '{item_ref}' is premiering, download will start in '{(starts_in.astimezone() + timedelta(minutes=5, seconds=duration)).isoformat()}'" f"Item '{item_ref}' is premiering, download will start in '{(starts_in + timedelta(minutes=5, seconds=duration)).astimezone().isoformat()}'"
) )
continue continue

View file

@ -44,7 +44,6 @@ class HttpAPI:
"config": self.config, "config": self.config,
"notify": self._notify, "notify": self._notify,
"cache": self.cache, "cache": self.cache,
"app": self.app,
"http_api": self, "http_api": self,
"root_path": self.rootPath, "root_path": self.rootPath,
}.items() }.items()

View file

@ -1,9 +1,11 @@
import inspect import inspect
import logging
from typing import Any, TypeVar from typing import Any, TypeVar
from app.library.Singleton import Singleton from app.library.Singleton import Singleton
T = TypeVar("T") T = TypeVar("T")
LOG: logging.Logger = logging.getLogger(__name__)
class Services(metaclass=Singleton): class Services(metaclass=Singleton):
@ -50,6 +52,10 @@ class Services(metaclass=Singleton):
sig = inspect.signature(handler) sig = inspect.signature(handler)
expected_args = sig.parameters.keys() expected_args = sig.parameters.keys()
filtered = {k: v for k, v in context.items() if k in expected_args} filtered = {k: v for k, v in context.items() if k in expected_args}
if missing_args := expected_args - filtered.keys():
LOG.error(f"Missing arguments for handler '{handler.__name__}': {missing_args}")
return await handler(**filtered) return await handler(**filtered)
def handle_sync(self, handler: callable, **kwargs) -> Any: def handle_sync(self, handler: callable, **kwargs) -> Any:
@ -58,4 +64,8 @@ class Services(metaclass=Singleton):
sig = inspect.signature(handler) sig = inspect.signature(handler)
expected_args = sig.parameters.keys() expected_args = sig.parameters.keys()
filtered = {k: v for k, v in context.items() if k in expected_args} filtered = {k: v for k, v in context.items() if k in expected_args}
if missing_args := expected_args - filtered.keys():
LOG.error(f"Missing arguments for handler '{handler.__name__}': {missing_args}")
return handler(**filtered) return handler(**filtered)

View file

@ -152,7 +152,7 @@ class Tasks(metaclass=Singleton):
self._tasks.append(task) self._tasks.append(task)
if not task.timer: if not task.timer or "[only_handler]" in task.name:
continue continue
try: try:

View file

@ -25,6 +25,7 @@ from app.library.HttpSocket import HttpSocket
from app.library.Notifications import Notification from app.library.Notifications import Notification
from app.library.Presets import Presets from app.library.Presets import Presets
from app.library.Scheduler import Scheduler from app.library.Scheduler import Scheduler
from app.library.Services import Services
from app.library.Tasks import Tasks from app.library.Tasks import Tasks
LOG = logging.getLogger("app") LOG = logging.getLogger("app")
@ -38,6 +39,8 @@ class Main:
self._config = Config.get_instance(is_native=is_native) self._config = Config.get_instance(is_native=is_native)
self._app = web.Application() self._app = web.Application()
Services.get_instance().add("app", self._app)
if self._config.debug: if self._config.debug:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.set_debug(True) loop.set_debug(True)

View file

@ -332,3 +332,7 @@ hr {
.is-bold { .is-bold {
font-weight: bold; font-weight: bold;
} }
.fa-spin-10 {
--fa-animation-iteration-count: 10;
}

View file

@ -22,17 +22,16 @@
</span> </span>
</button> </button>
</div> </div>
<div class="column is-half-mobile" v-if="hasDownloaded"> <div class="column is-half-mobile" v-if="hasDownloaded && hasSelected">
<button type="button" class="button is-fullwidth is-link" :disabled="!hasSelected" @click="downloadSelected"> <button type="button" class="button is-fullwidth is-link" @click="downloadSelected">
<span class="icon-text is-block"> <span class="icon-text is-block">
<span class="icon"><i class="fa-solid fa-download" /></span> <span class="icon"><i class="fa-solid fa-download" /></span>
<span>Download</span> <span>Download</span>
</span> </span>
</button> </button>
</div> </div>
<div class="column is-half-mobile"> <div class="column is-half-mobile" v-if="hasSelected">
<button type="button" class="button is-fullwidth is-danger" :disabled="!hasSelected" <button type="button" class="button is-fullwidth is-danger" @click="deleteSelectedItems">
@click="deleteSelectedItems">
<span class="icon-text is-block"> <span class="icon-text is-block">
<span class="icon"><i class="fa-solid fa-trash-can" /></span> <span class="icon"><i class="fa-solid fa-trash-can" /></span>
<span>{{ config.app.remove_files ? 'Remove' : 'Clear' }}</span> <span>{{ config.app.remove_files ? 'Remove' : 'Clear' }}</span>
@ -154,16 +153,6 @@
</td> </td>
<td class="is-vcentered is-items-center"> <td class="is-vcentered is-items-center">
<div class="field is-grouped is-grouped-centered"> <div class="field is-grouped is-grouped-centered">
<div class="control" v-if="('finished' === item.status && item.filename) || isEmbedable(item.url)">
<button v-if="'finished' === item.status && item.filename" @click="playVideo(item)"
v-tooltip="'Play video'" class="button is-danger is-light is-small">
<span class="icon"><i class="fa-solid fa-play" /></span>
</button>
<button v-else @click="embed_url = getEmbedable(item.url)" v-tooltip="'Play video'"
class="button is-danger is-small">
<span class="icon"><i class="fa-solid fa-play" /></span>
</button>
</div>
<div class="control" v-if="item.status != 'finished' || !item.filename"> <div class="control" v-if="item.status != 'finished' || !item.filename">
<button class="button is-warning is-fullwidth is-small" v-tooltip="'Re-queue video'" <button class="button is-warning is-fullwidth is-small" v-tooltip="'Re-queue video'"
@click="(event) => reQueueItem(item, event)"> @click="(event) => reQueueItem(item, event)">
@ -182,9 +171,23 @@
<span class="icon"><i class="fa-solid fa-trash-can" /></span> <span class="icon"><i class="fa-solid fa-trash-can" /></span>
</button> </button>
</div> </div>
<div class="control" v-if="item.url && !config.app.basic_mode"> <div class="control is-expanded" v-if="item.url && !config.app.basic_mode">
<Dropdown icons="fa-solid fa-cogs" label="" @open_state="s => table_container = !s" <Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s"
:button_classes="'is-small'"> :button_classes="'is-small'" label="Actions">
<template v-if="'finished' === item.status && item.filename">
<NuxtLink @click="playVideo(item)" class="dropdown-item">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Play video</span>
</NuxtLink>
<hr class="dropdown-divider" />
</template>
<template v-else-if="isEmbedable(item.url)">
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url)">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Play video</span>
</NuxtLink>
<hr class="dropdown-divider" />
</template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)"> <NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>Information</span> <span>Information</span>
@ -235,27 +238,26 @@
</div> </div>
<div class="card-header-icon"> <div class="card-header-icon">
<span class="tag is-info" v-if="item.extras?.duration"> <div class="field is-grouped">
{{ formatTime(item.extras.duration) }} <div class="control">
</span> <span class="tag is-info" v-if="item.extras?.duration">
{{ formatTime(item.extras.duration) }}
</span>
</div>
<div class="control">
<button @click="hideThumbnail = !hideThumbnail" v-if="thumbnails">
<span class="icon"><i class="fa-solid"
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail, }" /></span>
</button>
</div>
<div class="control">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms"
:id="'checkbox-' + item._id" :value="item._id">
</label>
</div>
<span v-if="!showThumbnails"> </div>
<a v-if="'finished' === item.status && item.filename" href="#" @click.prevent="playVideo(item)"
v-tooltip="'Play video.'">
<span class="icon"><i class="fa-solid fa-play" /></span>
</a>
<a v-else-if="isEmbedable(item.url)" href="#" @click.prevent="embed_url = getEmbedable(item.url)"
v-tooltip="'Play video.'" class="has-text-danger">
<span class="icon"><i class="fa-solid fa-play" /></span>
</a>
</span>
<a :href="item.url" class="has-text-primary" v-tooltip="'Copy url.'" @click.prevent="copyText(item.url)">
<span class="icon"><i class="fa-solid fa-copy" /></span>
</a>
<button @click="hideThumbnail = !hideThumbnail" v-if="thumbnails">
<span class="icon"><i class="fa-solid"
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail, }" /></span>
</button>
</div> </div>
</header> </header>
<div v-if="showThumbnails" class="card-image"> <div v-if="showThumbnails" class="card-image">
@ -298,10 +300,10 @@
</span> </span>
</div> </div>
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable" <div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"
v-if="item.live_in && 'not_live' === item.status"> v-if="'not_live' === item.status && (item.live_in || item.extras?.release_in)">
<span :date-datetime="item.live_in" class="user-hint" <span :date-datetime="item.live_in || item.extras?.release_in" class="user-hint"
v-tooltip="'Will automatically be requeued at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')" v-tooltip="'Will be downloaded at: ' + moment(item.live_in || item.extras?.release_in).format('YYYY-M-DD H:mm Z')"
v-rtime="item.live_in" /> v-rtime="item.live_in || item.extras?.release_in" />
</div> </div>
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"> <div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
<span class="user-hint" :date-datetime="item.datetime" <span class="user-hint" :date-datetime="item.datetime"
@ -311,13 +313,6 @@
v-if="item.file_size"> v-if="item.file_size">
{{ formatBytes(item.file_size) }} {{ formatBytes(item.file_size) }}
</div> </div>
<div class="column is-half-mobile has-text-centered is-unselectable">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
:value="item._id">
Select
</label>
</div>
</div> </div>
<div class="columns is-mobile is-multiline"> <div class="columns is-mobile is-multiline">
<div class="column is-half-mobile" v-if="item.status != 'finished' || !item.filename"> <div class="column is-half-mobile" v-if="item.status != 'finished' || !item.filename">
@ -346,8 +341,24 @@
</span> </span>
</a> </a>
</div> </div>
<div class="column is-half-mobile" v-if="item.url && !config.app.basic_mode"> <div class="column" v-if="item.url && !config.app.basic_mode">
<Dropdown icons="fa-solid fa-cogs" label="Actions"> <Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="'finished' === item.status && item.filename">
<NuxtLink @click="playVideo(item)" class="dropdown-item">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Play video</span>
</NuxtLink>
<hr class="dropdown-divider" />
</template>
<template v-else-if="isEmbedable(item.url)">
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url)">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Play video</span>
</NuxtLink>
<hr class="dropdown-divider" />
</template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)"> <NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>Information</span> <span>Information</span>
@ -417,12 +428,12 @@
</template> </template>
<script setup> <script setup>
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { makeDownload, formatBytes, uri } from '~/utils/index'
import { isEmbedable, getEmbedable } from '~/utils/embedable'
import Dropdown from './Dropdown.vue'
import { NuxtLink } from '#components' import { NuxtLink } from '#components'
import { useStorage } from '@vueuse/core'
import moment from 'moment'
import { getEmbedable, isEmbedable } from '~/utils/embedable'
import { formatBytes, makeDownload, uri } from '~/utils/index'
import Dropdown from './Dropdown.vue'
const emitter = defineEmits(['getInfo', 'add_new']) const emitter = defineEmits(['getInfo', 'add_new'])
@ -849,6 +860,6 @@ const is_queued = item => {
return '' return ''
} }
return item.live_in || item.extras?.live_in || item.extras?.release_in ? 'fa-spin' : '' return item.live_in || item.extras?.live_in || item.extras?.release_in ? 'fa-spin fa-spin-10' : ''
} }
</script> </script>

View file

@ -89,7 +89,7 @@
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode"> <div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode">
<div class="field"> <div class="field">
<label class="label is-inline is-unselectable"> <label class="label is-inline is-unselectable">
<span class="icon"><i class="fa-solid fa-comment" /></span> <span class="icon"><i class="fa-solid fa-object-ungroup" /></span>
<span>URLs Separator</span> <span>URLs Separator</span>
</label> </label>
<div class="control"> <div class="control">
@ -103,7 +103,7 @@
</div> </div>
<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>Use this separate multiple URLs in the input field.</span> <span>Use this to separate multiple URLs in the input field.</span>
</span> </span>
</div> </div>
</div> </div>

View file

@ -142,17 +142,31 @@
<NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink> <NuxtLink target="_blank" :href="item.url">{{ item.title }}</NuxtLink>
</div> </div>
<div class="card-header-icon"> <div class="card-header-icon">
<span class="tag is-info" v-if="item.extras?.duration"> <div class="field is-grouped">
{{ formatTime(item.extras.duration) }}
</span>
<a :href="item.url" class="has-text-primary" v-tooltip="'Copy url.'" @click.prevent="copyText(item.url)"> <div class="control">
<span class="icon"><i class="fa-solid fa-copy" /></span> <span class="tag is-info" v-if="item.extras?.duration">
</a> {{ formatTime(item.extras.duration) }}
<button @click="hideThumbnail = !hideThumbnail" v-if="thumbnails"> </span>
<span class="icon"><i class="fa-solid" </div>
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail, }" /></span> <div class="control" v-if="!showThumbnails && isEmbedable(item.url)">
</button> <NuxtLink @click="embed_url = getEmbedable(item.url)" v-tooltip="'Play video'">
<span class="icon has-text-danger"><i class="fa-solid fa-play" /></span>
</NuxtLink>
</div>
<div class="control">
<button @click="hideThumbnail = !hideThumbnail" v-if="thumbnails">
<span class="icon"><i class="fa-solid"
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail }" /></span>
</button>
</div>
<div class="control">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms"
:id="'checkbox-' + item._id" :value="item._id">
</label>
</div>
</div>
</div> </div>
</header> </header>
<div v-if="showThumbnails" class="card-image"> <div v-if="showThumbnails" class="card-image">
@ -194,29 +208,19 @@
v-if="item.downloaded_bytes"> v-if="item.downloaded_bytes">
{{ formatBytes(item.downloaded_bytes) }} {{ formatBytes(item.downloaded_bytes) }}
</div> </div>
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
:value="item._id">
Select
</label>
</div>
</div> </div>
<div class="columns is-multiline is-mobile"> <div class="columns is-multiline is-mobile">
<div class="column is-half-mobile"> <div class="column is-half-mobile">
<button class="button is-warning is-fullwidth" @click="confirmCancel(item);"> <button class="button is-warning is-fullwidth" @click="confirmCancel(item);">
<span class="icon-text is-block"> <span class="icon"><i class="fa-solid fa-eject" /></span>
<span class="icon"><i class="fa-solid fa-eject" /></span> <span>Cancel</span>
<span>Cancel</span>
</span>
</button> </button>
</div> </div>
<div class="column is-half-mobile" v-if="item.url && !config.app.basic_mode"> <div class="column is-half-mobile" v-if="item.url && !config.app.basic_mode">
<button class="button is-info is-fullwidth" @click="emitter('getInfo', item.url)"> <button class="button is-info is-fullwidth" @click="emitter('getInfo', item.url)">
<span class="icon-text is-block"> <span class="icon"><i class="fa-solid fa-info" /></span>
<span class="icon"><i class="fa-solid fa-info" /></span> <span>Information</span>
<span>Information</span>
</span>
</button> </button>
</div> </div>
</div> </div>

View file

@ -250,6 +250,8 @@
and automatically queues any you havent downloaded yet.</li> and automatically queues any you havent downloaded yet.</li>
<li>To opt out of RSS monitoring for a specific task, append <code>[no_handler]</code> to that tasks name. <li>To opt out of RSS monitoring for a specific task, append <code>[no_handler]</code> to that tasks name.
</li> </li>
<li>To have the task only monitor RSS, set a timer and add <code>[only_handler]</code> to that tasks name.
</li>
<li>RSS Feed monitoring will only work if you have <code>--download-archive</code> set in command options <li>RSS Feed monitoring will only work if you have <code>--download-archive</code> set in command options
for ytdlp.cli, preset or task.</li> for ytdlp.cli, preset or task.</li>
<li>If you don't have <code>--download-archive</code> set but <code>YTP_KEEP_ARCHIVE</code> environment <li>If you don't have <code>--download-archive</code> set but <code>YTP_KEEP_ARCHIVE</code> environment

View file

@ -23,7 +23,7 @@
</p> </p>
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode"> <p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode">
<button v-tooltip.bottom="'Toggle new download form'" class="button is-primary has-tooltip-bottom" <button v-tooltip.bottom="'Toggle download form'" class="button is-primary has-tooltip-bottom"
@click="config.showForm = !config.showForm"> @click="config.showForm = !config.showForm">
<span class="icon"><i class="fa-solid fa-plus" /></span> <span class="icon"><i class="fa-solid fa-plus" /></span>
</button> </button>