Merge pull request #431 from arabcoders/dev
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled

Add experimental simple view mode
This commit is contained in:
Abdulmohsen 2025-09-27 21:05:58 +03:00 committed by GitHub
commit 1103b93f00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1069 additions and 251 deletions

View file

@ -47,8 +47,10 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v4 uses: astral-sh/setup-uv@v6.7
with: with:
enable-cache: true
save-cache: true
version: "latest" version: "latest"
- name: Install Python dependencies - name: Install Python dependencies

View file

@ -45,6 +45,9 @@ class Config(metaclass=Singleton):
temp_path: str = "/tmp" temp_path: str = "/tmp"
"""The path to the temporary directory.""" """The path to the temporary directory."""
simple_mode: bool = False
"""Enable simple mode."""
temp_keep: bool = False temp_keep: bool = False
"""Keep temporary files after the download is complete.""" """Keep temporary files after the download is complete."""
@ -244,6 +247,7 @@ class Config(metaclass=Singleton):
"prevent_premiere_live", "prevent_premiere_live",
"temp_disabled", "temp_disabled",
"allow_internal_urls", "allow_internal_urls",
"simple_mode",
) )
"The variables that are booleans." "The variables that are booleans."
@ -259,6 +263,7 @@ class Config(metaclass=Singleton):
"default_preset", "default_preset",
"instance_title", "instance_title",
"console_enabled", "console_enabled",
"simple_mode",
"browser_control_enabled", "browser_control_enabled",
"file_logging", "file_logging",
"base_path", "base_path",

View file

@ -358,6 +358,10 @@ div.is-centered {
justify-content: center; justify-content: center;
} }
.is-overflow-visible {
overflow: visible !important
}
.modal-content-max { .modal-content-max {
width: calc(100% - 20px); width: calc(100% - 20px);
max-width: 1344px; max-width: 1344px;

View file

@ -268,6 +268,7 @@ import { useStorage } from '@vueuse/core'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue' import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete'; import type { AutoCompleteOptions } from '~/types/autocomplete';
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions' import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits<{ const emitter = defineEmits<{
(e: 'cancel'): void (e: 'cancel'): void
@ -410,7 +411,7 @@ const importItem = async (): Promise<void> => {
return return
} }
if ((form.filter || form.cli || Object.keys(form.extras).length > 0) && !(await box.confirm('Overwrite the current form fields?', true))) { if ((form.filter || form.cli || Object.keys(form.extras).length > 0) && !(await box.confirm('Overwrite the current form fields?'))) {
return return
} }

View file

@ -450,6 +450,7 @@
import moment from 'moment' import moment from 'moment'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits<{ const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void (e: 'getInfo', url: string, preset: string, cli: string): void
@ -588,7 +589,7 @@ const deleteSelectedItems = async () => {
if (true === config.app.remove_files) { if (true === config.app.remove_files) {
msg += ' This will remove any associated files if they exists.' msg += ' This will remove any associated files if they exists.'
} }
if (false === (await box.confirm(msg, config.app.remove_files))) { if (false === (await box.confirm(msg))) {
return return
} }
for (const key in selectedElms.value) { for (const key in selectedElms.value) {
@ -750,7 +751,7 @@ const removeItem = async (item: StoreItem) => {
if (item.status === 'finished' && config.app.remove_files) { if (item.status === 'finished' && config.app.remove_files) {
msg += ' This will remove any associated files if they exists.' msg += ' This will remove any associated files if they exists.'
} }
if (false === (await box.confirm(msg, Boolean(item.filename && config.app.remove_files)))) { if (false === (await box.confirm(msg))) {
return false return false
} }

View file

@ -308,6 +308,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { notification, notificationImport } from '~/types/notification' import type { notification, notificationImport } from '~/types/notification'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits(['cancel', 'submit']) const emitter = defineEmits(['cancel', 'submit'])
const toast = useNotification() const toast = useNotification()
@ -412,7 +413,7 @@ const importItem = async () => {
} }
if (form.name || form.request?.url) { if (form.name || form.request?.url) {
if (false === (await box.confirm('Overwrite the current form fields?', true))) { if (false === (await box.confirm('Overwrite the current form fields?'))) {
return return
} }
} }

View file

@ -313,6 +313,7 @@
import moment from 'moment' import moment from 'moment'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits<{ const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void (e: 'getInfo', url: string, preset: string, cli: string): void

View file

@ -12,6 +12,25 @@
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-6"> <div class="column is-6">
<div class="field">
<label class="label is-unselectable">
<span class="icon"><i class="fa-solid fa-computer" /></span>
Page View
</label>
<div class="control">
<div class="control">
<input id="random_bg" type="checkbox" class="switch is-success" v-model="simpleMode">
<label for="random_bg" class="is-unselectable">
&nbsp;{{ simpleMode ? 'Simple View (Experimental)' : 'Regular View (Default)' }}
</label>
</div>
</div>
<p class="help is-bold has-text-danger">
<span class="icon"> <i class="fa-solid fa-info-circle" /></span>
The simple view is experimental and will change without notice and maybe even deleted in the future.
</p>
</div>
<div class="field"> <div class="field">
<label class="label is-unselectable">Color scheme</label> <label class="label is-unselectable">Color scheme</label>
<div class="control"> <div class="control">
@ -35,7 +54,7 @@
<div class="field"> <div class="field">
<label class="label is-unselectable"> <label class="label is-unselectable">
Backgrounds Show Background
<template v-if="bg_enable"> <template v-if="bg_enable">
<NuxtLink @click="$emit('reload_bg')" class="is-bold">Reload</NuxtLink> <NuxtLink @click="$emit('reload_bg')" class="is-bold">Reload</NuxtLink>
<span class="icon" v-if="isLoading"><i class="fa fa-spin fa-spinner" /></span> <span class="icon" v-if="isLoading"><i class="fa fa-spin fa-spinner" /></span>
@ -44,7 +63,7 @@
<div class="control"> <div class="control">
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable"> <input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
<label for="random_bg" class="is-unselectable"> <label for="random_bg" class="is-unselectable">
&nbsp;{{ bg_enable ? 'Enabled' : 'Disabled' }} &nbsp;{{ bg_enable ? 'Yes' : 'No' }}
</label> </label>
</div> </div>
</div> </div>
@ -59,17 +78,7 @@
</div> </div>
</div> </div>
<div class="field"> <div class="field" v-if="!simpleMode">
<label class="label is-unselectable" for="show_thumbnail">Show Videos Thumbnail when possible</label>
<div class="control">
<input id="show_thumbnail" type="checkbox" class="switch is-success" v-model="show_thumbnail">
<label for="show_thumbnail" class="is-unselectable">
&nbsp;{{ show_thumbnail ? 'Enabled' : 'Disabled' }}
</label>
</div>
</div>
<div class="field">
<label class="label is-unselectable" for="show_thumbnail">URLs Separator</label> <label class="label is-unselectable" for="show_thumbnail">URLs Separator</label>
<div class="control"> <div class="control">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
@ -85,27 +94,28 @@
</div> </div>
<div class="column is-6"> <div class="column is-6">
<div class="field"> <div class="field">
<label class="label is-unselectable" for="reduce_confirm">Reduce confirm box usage</label> <label class="label is-unselectable" for="show_thumbnail">Show Thumbnails</label>
<div class="control"> <div class="control">
<input id="reduce_confirm" type="checkbox" class="switch is-success" v-model="reduce_confirm"> <input id="show_thumbnail" type="checkbox" class="switch is-success" v-model="show_thumbnail">
<label for="reduce_confirm" class="is-unselectable"> <label for="show_thumbnail" class="is-unselectable">
&nbsp;{{ reduce_confirm ? 'Enabled' : 'Disabled' }} &nbsp;{{ show_thumbnail ? 'Yes' : 'No' }}
</label> </label>
</div> </div>
<p class="help is-bold"> Show videos thumbnail if available.</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="allow_toasts">Show notifications toasts</label> <label class="label" for="allow_toasts">Show notifications</label>
<div class="control"> <div class="control">
<input id="allow_toasts" type="checkbox" class="switch is-success" v-model="allow_toasts"> <input id="allow_toasts" type="checkbox" class="switch is-success" v-model="allow_toasts">
<label for="allow_toasts" class="is-unselectable"> <label for="allow_toasts" class="is-unselectable">
&nbsp;{{ allow_toasts ? 'Enabled' : 'Disabled' }} &nbsp;{{ allow_toasts ? 'Yes' : 'No' }}
</label> </label>
</div> </div>
</div> </div>
<div class="field" v-if="allow_toasts"> <div class="field" v-if="allow_toasts">
<label class="label">Toasts position</label> <label class="label">Notification position</label>
<div class="control"> <div class="control">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select v-model="toast_position"> <select v-model="toast_position">
@ -121,12 +131,12 @@
</div> </div>
<div class="field" v-if="allow_toasts"> <div class="field" v-if="allow_toasts">
<label class="label" for="dismiss_on_click">Dismiss toasts on click</label> <label class="label" for="dismiss_on_click">Dismiss notification on click</label>
<div class="control"> <div class="control">
<input id="dismiss_on_click" type="checkbox" class="switch is-success" <input id="dismiss_on_click" type="checkbox" class="switch is-success"
v-model="toast_dismiss_on_click"> v-model="toast_dismiss_on_click">
<label for="dismiss_on_click" class="is-unselectable"> <label for="dismiss_on_click" class="is-unselectable">
&nbsp;{{ toast_dismiss_on_click ? 'Enabled' : 'Disabled' }} &nbsp;{{ toast_dismiss_on_click ? 'Yes' : 'No' }}
</label> </label>
</div> </div>
</div> </div>
@ -142,6 +152,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { POSITION } from 'vue-toastification' import { POSITION } from 'vue-toastification'
import { useConfigStore } from '~/stores/ConfigStore'
defineProps<{ isLoading: boolean }>() defineProps<{ isLoading: boolean }>()
defineEmits<{ (e: 'reload_bg'): void }>() defineEmits<{ (e: 'reload_bg'): void }>()
@ -150,9 +161,9 @@ const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95) const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
const selectedTheme = useStorage<'auto' | 'light' | 'dark'>('theme', 'auto') const selectedTheme = useStorage<'auto' | 'light' | 'dark'>('theme', 'auto')
const allow_toasts = useStorage<boolean>('allow_toasts', true) const allow_toasts = useStorage<boolean>('allow_toasts', true)
const reduce_confirm = useStorage<boolean>('reduce_confirm', false)
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT) const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true) const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
const show_thumbnail = useStorage<boolean>('show_thumbnail', true) const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',') const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
const simpleMode = useStorage<boolean>('simple_mode', useConfigStore().app.simple_mode || false)
</script> </script>

View file

@ -0,0 +1,764 @@
<template>
<div class="basic-wrapper">
<div class="form-container" :class="{ 'is-centered': shouldCenterForm }">
<section class="download-form box">
<form class="download-form__body" autocomplete="off" @submit.prevent="addDownload">
<label class="label" for="download-url">
What you would like to download?
<span class="is-pulled-right">
<span class="icon has-text-primary is-pointer" @click="$emit('show_settings')" v-tooltip="'Settings'"><i
class="fas fa-cogs" /></span>
</span>
</label>
<div class="field has-addons">
<div class="control">
<button type="button" class="button is-info" @click="() => showPresets = !showPresets">
<span class="icon"><i class="fas" :class="showPresets ? 'fa-chevron-up' : 'fa-chevron-down'" /></span>
</button>
</div>
<div class="control is-expanded">
<input id="download-url" v-model="formUrl" :disabled="!socketStore.isConnected || addInProgress"
class="input" placeholder="https://..." type="url" required>
</div>
<div class="control">
<button type="submit" class="button is-primary" :class="{ 'is-loading': addInProgress }"
:disabled="!socketStore.isConnected || addInProgress || !formUrl.trim()">
<span class="icon"><i class="fas fa-plus" /></span>
<span>Add</span>
</button>
</div>
</div>
<div class="field has-addons" v-if="showPresets">
<div class="control">
<label class="button is-static">
<span class="icon"><i class="fas fa-sliders" /></span>
<span>Preset</span>
</label>
</div>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select id="preset" class="is-fullwidth" :disabled="!socketStore.isConnected || addInProgress"
v-model="formPreset.preset">
<optgroup label="Custom presets" v-if="presets.filter(p => !p?.default).length > 0">
<option v-for="cPreset in filter_presets(false)" :key="cPreset.name" :value="cPreset.name">
{{ cPreset.name }}
</option>
</optgroup>
<optgroup label="Default presets">
<option v-for="dPreset in filter_presets(true)" :key="dPreset.name" :value="dPreset.name">
{{ dPreset.name }}
</option>
</optgroup>
</select>
</div>
</div>
</div>
</form>
</section>
</div>
<Transition name="queue-fade">
<section v-if="hasAnyItems" key="queue" class="queue-section">
<TransitionGroup name="queue-card" tag="div" class="columns is-multiline queue-card-columns">
<div v-for="entry in displayItems" :key="entry.item._id" class="column is-12-mobile is-6-tablet">
<article class="queue-card card" :class="{ 'is-history': 'history' === entry.source }">
<div v-if="'queue' === entry.source && shouldShowProgress(entry.item)"
class="progress-bar is-unselectable mb-3">
<div class="progress-percentage">{{ updateProgress(entry.item) }}</div>
<div class="progress" :style="{ width: getProgressWidth(entry.item) }"></div>
</div>
<div class="card-content">
<article class="media">
<figure class="media-left">
<figure class="image is-16by9 queue-thumb" :class="{ 'is-clickable': isEmbedable(entry.item.url) }"
role="presentation" @click="openPlayer(entry.item)">
<img :src="resolveThumbnail(entry)" :alt="entry.item.title || 'Video thumbnail'" loading="lazy">
<span v-if="getDurationLabel(entry.item)" class="queue-thumb__badge">
{{ getDurationLabel(entry.item) }}
</span>
<span
v-if="entry.item.filename && entry.item.status === 'finished' || isEmbedable(entry.item.url)"
class="queue-thumb__overlay">
<span class="icon circle-border"
:class="{ 'has-text-danger': isEmbedable(entry.item.url) && (!entry.item.filename || entry.item.status !== 'finished') }">
<i class="fas fa-play" />
</span>
</span>
</figure>
</figure>
<div class="media-content is-grid">
<p class="title is-6 mb-0 queue-title">
<NuxtLink target="_blank" :href="entry.item.url">{{ entry.item.title }}</NuxtLink>
</p>
<div class="field is-grouped is-unselectable">
<div class="control">
<span class="tag is-size-7 has-text-weight-semibold is-uppercase"
:class="getSourceTagClass(entry)">
{{ getSourceLabel(entry) }}
</span>
</div>
<div class="control">
<span class="tag is-size-7 has-text-weight-semibold" :class="getStatusClass(entry.item)">
{{ getStatusLabel(entry.item) }}
</span>
</div>
<div class="control">
<span class="tag" :date-datetime="entry.item.datetime" v-rtime="entry.item.datetime" />
</div>
</div>
<p class="content is-size-7 has-text-grey queue-description">
<template v-if="entry.item.error || showMessage(entry.item)">
<template v-if="entry.item.error">
<span class="has-text-danger">{{ entry.item.error }}</span>
</template>
<template v-if="showMessage(entry.item)">
<span class="has-text-danger">{{ entry.item.msg }}</span>
</template>
</template>
<template v-else>
{{ getDescription(entry.item) || 'No description available.' }}
</template>
</p>
</div>
</article>
<div v-if="'queue' === entry.source" class="buttons are-small queue-actions mt-3">
<button v-if="canStart(entry.item)" class="button is-success is-light" type="button"
@click="startQueueItem(entry.item)">
<span class="icon"><i class="fas fa-circle-play" /></span>
<span>Start</span>
</button>
<button v-if="canPause(entry.item)" class="button is-warning is-light" type="button"
@click="pauseQueueItem(entry.item)">
<span class="icon"><i class="fas fa-pause" /></span>
<span>Pause</span>
</button>
<button class="button is-warning" type="button" @click="cancelDownload(entry.item)">
<span class="icon"><i class="fas fa-xmark" /></span>
<span>Cancel</span>
</button>
</div>
<div v-else class="buttons are-small queue-actions mt-3">
<a v-if="getDownloadLink(entry.item)" class="button is-link" :href="getDownloadLink(entry.item)"
:download="getDownloadName(entry.item)">
<span class="icon"><i class="fas fa-download" /></span>
<span>Download</span>
</a>
<button v-if="entry.item.status != 'finished' || !entry.item.filename" class="button is-info is-light"
type="button" @click="requeueItem(entry.item)">
<span class="icon"><i class="fas fa-rotate-right" /></span>
<span>Requeue</span>
</button>
<button class="button is-danger" type="button" @click="deleteHistoryItem(entry.item)">
<span class="icon"><i class="fas fa-trash" /></span>
<span>Delete</span>
</button>
</div>
</div>
</article>
</div>
</TransitionGroup>
</section>
</Transition>
<div class="modal is-active" v-if="videoItem">
<div class="modal-background" @click="closePlayer"></div>
<div class="modal-content is-unbounded-model">
<VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="videoItem"
class="is-fullwidth" @closeModel="closePlayer" />
</div>
<button class="modal-close is-large" aria-label="close" @click="closePlayer"></button>
</div>
<div class="modal is-active" v-if="embedUrl">
<div class="modal-background" @click="closePlayer"></div>
<div class="modal-content is-unbounded-model">
<EmbedPlayer :url="embedUrl" @closeModel="closePlayer" />
</div>
<button class="modal-close is-large" aria-label="close" @click="closePlayer"></button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useStorage } from '@vueuse/core'
import type { item_request } from '~/types/item'
import type { ItemStatus, StoreItem } from '~/types/store'
import { useNotification } from '~/composables/useNotification'
import { useConfigStore } from '~/stores/ConfigStore'
import { useStateStore } from '~/stores/StateStore'
import { useSocketStore } from '~/stores/SocketStore'
import { isEmbedable, getEmbedable } from '~/utils/embedable'
import EmbedPlayer from '~/components/EmbedPlayer.vue'
import { ag, encodePath, formatTime, makeDownload, request, stripPath, ucFirst, uri } from '~/utils'
defineEmits<{ (e: 'show_settings'): void }>()
const configStore = useConfigStore()
const stateStore = useStateStore()
const socketStore = useSocketStore()
const toast = useNotification()
const { app, paused, presets } = storeToRefs(configStore)
const { queue, history } = storeToRefs(stateStore)
const embedUrl = ref<string>('')
const videoItem = ref<StoreItem | null>(null)
const formUrl = ref<string>('')
const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' })
const addInProgress = ref<boolean>(false)
const showPresets = ref<boolean>(false)
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const sortByNewest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
const queueItems = computed<StoreItem[]>(() => sortByNewest(Object.values(queue.value ?? {})))
const historyEntries = computed<StoreItem[]>(() => sortByNewest(Object.values(history.value ?? {})))
const downloadingStatuses: ReadonlySet<ItemStatus | null> = new Set(['downloading', 'postprocessing', 'preparing'])
const isDownloading = (status: ItemStatus | null): boolean => downloadingStatuses.has(status)
const downloadingItems = computed<StoreItem[]>(() => queueItems.value.filter(item => isDownloading(item.status)))
const queuedItems = computed<StoreItem[]>(() => queueItems.value.filter(item => !isDownloading(item.status)))
type DisplayEntry = { item: StoreItem; source: 'queue' | 'history' }
const displayItems = computed<DisplayEntry[]>(() => [
...downloadingItems.value.map(item => ({ item, source: 'queue' as const })),
...queuedItems.value.map(item => ({ item, source: 'queue' as const })),
...historyEntries.value.map(item => ({ item, source: 'history' as const })),
])
const hasActiveQueue = computed<boolean>(() => queueItems.value.length > 0)
const hasAnyItems = computed<boolean>(() => hasActiveQueue.value || historyEntries.value.length > 0)
const shouldCenterForm = computed<boolean>(() => 0 === queueItems.value.length && 0 === historyEntries.value.length)
const addDownload = async (): Promise<void> => {
const url = formUrl.value.trim()
if (!url) {
toast.error('Please enter a valid URL.')
return
}
const payload: item_request[] = [{
url,
preset: formPreset.value.preset || app.value.default_preset,
auto_start: true,
}]
try {
addInProgress.value = true
const response = await request('/api/history', {
method: 'POST',
body: JSON.stringify(payload),
})
const data = await response.json()
if (!response.ok) {
toast.error(data?.error ?? 'Failed to add download.')
return
}
const failures = Array.isArray(data) ? data.filter((item: Record<string, any>) => false === item?.status) : []
if (failures.length > 0) {
failures.forEach((item: Record<string, any>) => toast.error(item?.msg ?? 'Failed to add download.'))
return
}
formUrl.value = ''
formPreset.value.preset = app.value.default_preset || ''
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to add download.'
toast.error(message)
} finally {
addInProgress.value = false
}
}
const resolveThumbnail = (entry: DisplayEntry): string => {
if (!show_thumbnail.value) {
return '/images/placeholder.png'
}
const { item, source } = entry
const sidecarImage = item.sidecar?.image?.[0]?.file
if ('history' === source && sidecarImage) {
const relativePath = stripPath(app.value.download_path ?? '', sidecarImage)
return uri(`/api/download/${encodeURIComponent(relativePath)}`)
}
const remoteThumb = ag<string | null>(item, 'extras.thumbnail', null)
if (remoteThumb) {
return uri(`/api/thumbnail?id=${item._id}&url=${encodePath(remoteThumb)}`)
}
return '/images/placeholder.png'
}
const openPlayer = (item: StoreItem): void => {
if (item.filename && item.status === 'finished') {
videoItem.value = item
return
}
if (!isEmbedable(item.url)) {
return
}
const embed = getEmbedable(item.url)
if (embed) {
embedUrl.value = embed
}
}
const closePlayer = (): void => {
embedUrl.value = ''
videoItem.value = null
}
const getSourceLabel = (entry: DisplayEntry): string => {
if ('history' === entry.source) {
return 'History'
}
if (isDownloading(entry.item.status)) {
return 'Active'
}
return 'Queued'
}
const getSourceTagClass = (entry: DisplayEntry): string => {
if ('history' === entry.source) {
return 'is-light'
}
if (isDownloading(entry.item.status)) {
return 'is-info is-light'
}
return 'is-warning is-light'
}
const getDescription = (item: StoreItem): string => {
const direct = (item.description ?? '').toString().trim()
if (direct) {
return direct
}
const extrasDescription = ag<string | null>(item, 'extras.description', null)?.toString().trim()
if (extrasDescription) {
return extrasDescription
}
const errorDescription = item.error?.trim()
if (errorDescription) {
return errorDescription
}
const message = ag<string | null>(item, 'msg', null)?.toString().trim()
if (message) {
return message
}
return ''
}
const getDurationLabel = (item: StoreItem): string | null => {
const duration = ag<number | null>(item, 'extras.duration', null)
if (null == duration || Number.isNaN(duration) || 0 >= duration) {
return null
}
return formatTime(duration)
}
const statusOverrides: Record<string, string> = {
downloading: 'Downloading',
postprocessing: 'Post-processing',
preparing: 'Preparing',
finished: 'Completed',
error: 'Error',
cancelled: 'Cancelled',
not_live: 'Not live yet',
skip: 'Skipped',
}
const getStatusLabel = (item: StoreItem): string => {
if (null === item.status) {
return 'Queued'
}
return statusOverrides[item.status] ?? ucFirst(item.status.replace(/_/g, ' '))
}
const statusColorMap: Record<string, string> = {
downloading: 'has-text-info',
postprocessing: 'has-text-link',
preparing: 'has-text-link',
finished: 'has-text-success',
error: 'has-text-danger',
cancelled: 'has-text-grey',
not_live: 'has-text-warning',
skip: 'has-text-grey',
}
const getStatusClass = (item: StoreItem): string => {
if (null === item.status) {
return 'has-text-grey'
}
return statusColorMap[item.status] ?? 'has-text-info'
}
const shouldShowProgress = (item: StoreItem): boolean => isDownloading(item.status) || null === item.status
const percentPipe = (value: number | null): string => {
if (null === value || Number.isNaN(value)) {
return '00.00'
}
return parseFloat(String(value)).toFixed(2)
}
const ETAPipe = (value: number | null): string => {
if (null === value || 0 === value) {
return 'Live'
}
if (value < 60) {
return `${Math.round(value)}s`
}
if (value < 3600) {
return `${Math.floor(value / 60)}m ${Math.round(value % 60)}s`
}
const hours = Math.floor(value / 3600)
const minutes = value % 3600
return `${hours}h ${Math.floor(minutes / 60)}m ${Math.round(minutes % 60)}s`
}
const speedPipe = (value: number | null): string => {
if (null === value || 0 === value) {
return '0KB/s'
}
const k = 1024
const dm = 2
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s']
const i = Math.floor(Math.log(value) / Math.log(k))
return `${parseFloat((value / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
const updateProgress = (item: StoreItem): string => {
let text = ''
if (!item.auto_start) {
return 'Manual start'
}
if (null === item.status && true === paused.value) {
return 'Global Pause'
}
if ('postprocessing' === item.status) {
return 'Post-processors are running.'
}
if ('preparing' === item.status) {
return ag(item, 'extras.external_downloader') ? 'External downloader.' : 'Preparing'
}
if (null != item.status) {
text += item.percent && !item.is_live ? `${percentPipe(item.percent)}%` : 'Live'
}
text += item.speed ? ` - ${speedPipe(item.speed)}` : ' - Waiting..'
if (null != item.status && item.eta) {
text += ` - ${ETAPipe(item.eta)}`
}
return text
}
const getProgressWidth = (item: StoreItem): string => {
const percent = parseFloat(percentPipe(item.percent ?? 0))
const clamped = Number.isNaN(percent) ? 0 : Math.min(100, Math.max(0, percent))
return `${clamped}%`
}
const canStart = (item: StoreItem): boolean => !item.status && false === item.auto_start
const canPause = (item: StoreItem): boolean => !item.status && true === item.auto_start
const startQueueItem = (item: StoreItem): void => {
socketStore.emit('item_start', item._id)
}
const pauseQueueItem = (item: StoreItem): void => {
socketStore.emit('item_pause', item._id)
}
const cancelDownload = (item: StoreItem): void => {
socketStore.emit('item_cancel', item._id)
}
const getDownloadLink = (item: StoreItem): string => {
if (!item.filename) {
return ''
}
return makeDownload(app.value, item)
}
const getDownloadName = (item: StoreItem): string => {
if (!item.filename) {
return 'download'
}
const segments = item.filename.split('/')
return segments[segments.length - 1] || 'download'
}
const requeueItem = (item: StoreItem): void => {
if (!item.url) {
toast.error('Unable to requeue item; missing URL.')
return
}
const payload: item_request = {
url: item.url,
preset: item.preset || app.value.default_preset,
folder: item.folder,
template: item.template,
cookies: item.cookies,
cli: item.cli,
auto_start: item.auto_start ?? true,
}
if (item.extras && Object.keys(item.extras).length > 0) {
payload.extras = JSON.parse(JSON.stringify(item.extras))
}
socketStore.emit('item_delete', { id: item._id, remove_file: false })
socketStore.emit('add_url', payload)
}
const deleteHistoryItem = (item: StoreItem): void => {
socketStore.emit('item_delete', { id: item._id, remove_file: app.value.remove_files })
toast.info('Removed from history queue.')
}
const filter_presets = (flag: boolean = true) => presets.value.filter(item => item.default === flag)
const showMessage = (item: StoreItem) => {
if (!item?.msg || item.msg === item?.error) {
return false
}
return (item.msg?.length || 0) > 0
}
</script>
<style scoped>
.basic-wrapper {
min-height: calc(100vh - 6rem);
display: flex;
flex-direction: column;
align-items: center;
gap: 2.5rem;
padding: 2rem 1rem 4rem;
}
.form-container {
width: 100%;
display: flex;
justify-content: center;
transition: transform 0.45s cubic-bezier(0.25, 0.8, 0.25, 1),
filter 0.35s ease;
will-change: transform;
}
.form-container.is-centered {
transform: translateY(30vh);
filter: drop-shadow(0 18px 32px rgba(10, 10, 10, 0.25));
}
.download-form {
width: min(560px, 100%);
transition: box-shadow 0.45s ease;
}
.download-form__body {
display: flex;
flex-direction: column;
gap: 1rem;
}
.queue-section {
width: min(1100px, 100%);
display: flex;
flex-direction: column;
gap: 2rem;
transform-origin: top center;
}
.queue-card-columns {
margin-top: 0.75rem;
}
.queue-card {
height: 100%;
transition: transform 0.3s ease, box-shadow 0.35s ease;
}
.queue-card:hover {
transform: translateY(-4px);
box-shadow: 0 14px 30px rgba(10, 10, 10, 0.12);
}
.queue-card .card-content {
display: flex;
flex-direction: column;
height: 100%;
}
.queue-card .media-left {
margin-right: 1rem;
}
.queue-thumb {
position: relative;
width: 12rem;
max-width: 100%;
border-radius: 0.5rem;
overflow: hidden;
}
.queue-thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.queue-thumb.is-clickable {
cursor: pointer;
}
.queue-thumb__overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0);
color: #fff;
transition: background-color 0.2s ease;
pointer-events: none;
z-index: 0;
}
.queue-thumb.is-clickable:hover .queue-thumb__overlay {
background: rgba(0, 0, 0, 0.45);
}
.queue-thumb__badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.15rem 0.5rem;
border-radius: 999px;
background: rgba(0, 0, 0, 0.65);
color: #fff;
font-size: 0.7rem;
line-height: 1;
z-index: 1;
}
.queue-title {
overflow: hidden;
}
.queue-title a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.queue-description {
max-height: 4.5rem;
min-height: 2.5rem;
overflow: hidden;
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-word;
overflow-wrap: anywhere;
}
.queue-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.5rem;
margin-top: auto;
}
.queue-fade-enter-active,
.queue-fade-leave-active {
transition: opacity 0.35s ease, transform 0.45s ease;
}
.queue-fade-enter-from,
.queue-fade-leave-to {
opacity: 0;
transform: translateY(18px);
}
.queue-card-enter-active,
.queue-card-leave-active {
transition: opacity 0.3s ease, transform 0.35s ease;
}
.queue-card-enter-from,
.queue-card-leave-to {
opacity: 0;
transform: translateY(12px);
}
.queue-card-leave-active {
position: absolute;
}
@media (max-width: 768px) {
.form-container.is-centered {
transform: translateY(18vh);
}
.queue-card .media {
flex-direction: column;
align-items: flex-start;
}
.queue-card .media-left {
margin-right: 0;
margin-bottom: 1rem;
width: 100%;
}
.queue-thumb {
width: 100%;
}
.queue-card .media-content {
width: 100%;
}
.queue-title {
width: 100%;
}
.queue-actions {
width: 100%;
justify-content: flex-end;
}
}
.progress-bar {
border-radius: 0.75rem 0.75rem 0 0
}
.circle-border {
border: 2px solid;
border-radius: 50%;
padding: 0.5rem;
}
</style>

View file

@ -307,6 +307,7 @@ import { CronExpressionParser } from 'cron-parser'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue' import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete' import type { AutoCompleteOptions } from '~/types/autocomplete'
import type { exported_task, task_item } from '~/types/tasks' import type { exported_task, task_item } from '~/types/tasks'
import {useConfirm} from '~/composables/useConfirm'
const props = defineProps<{ const props = defineProps<{
reference?: string | null | undefined reference?: string | null | undefined
@ -408,7 +409,7 @@ const importItem = async (): Promise<void> => {
} }
if (form.url || form.timer) { if (form.url || form.timer) {
if (false === (await box.confirm('Overwrite the current form fields?', true))) { if (false === (await box.confirm('Overwrite the current form fields?'))) {
return return
} }
} }

View file

@ -1,42 +1,29 @@
import { useStorage } from '@vueuse/core' import { useDialog, type ConfirmOptions, type AlertOptions, type PromptOptions } from './useDialog'
import { useDialog } from './useDialog'
const dialog = useDialog() const dialog = useDialog()
const reduceConfirm = useStorage<boolean>('reduce_confirm', false) const confirm = async (msg: string, opts: ConfirmOptions = {}) => {
const { status } = await dialog.confirmDialog(Object.assign({
const confirm = async (msg: string, force: boolean = false) => {
if (false === force && true === reduceConfirm.value) {
return true
}
const { status } = await dialog.confirmDialog({
title: 'Please Confirm', title: 'Please Confirm',
message: msg, message: msg,
cancelText: 'Cancel', cancelText: 'Cancel',
confirmText: 'OK', confirmText: 'OK',
}) } as ConfirmOptions, opts || {}))
return status return status
} }
const alert = async (msg: string) => { const alert = async (msg: string, opts: AlertOptions = {}) => {
const { status } = await dialog.alertDialog({ const { status } = await dialog.alertDialog(Object.assign({
title: 'Alert', title: 'Alert',
message: msg, message: msg,
confirmText: 'OK', confirmText: 'OK',
}) } as AlertOptions, opts || {}))
return status return status
} }
const prompt = async (msg: string, defaultValue: string = '') => { const prompt = async (msg: string, opts: PromptOptions = {}) => {
const { status, value } = await dialog.promptDialog({ const { status, value } = await dialog.promptDialog(Object.assign({ message: msg } as PromptOptions, opts || {}))
title: 'Input Required',
message: msg,
initial: defaultValue,
cancelText: 'Cancel',
confirmText: 'OK',
})
if (status) { if (status) {
return value return value
@ -45,6 +32,4 @@ const prompt = async (msg: string, defaultValue: string = '') => {
return null return null
} }
export default function useConfirm() { export const useConfirm = () => ({ confirm, alert, prompt })
return { confirm, alert, prompt, reduceConfirm }
}

View file

@ -1,4 +1,16 @@
<template> <template>
<template v-if="simpleMode">
<Simple @show_settings="() => show_settings = true" />
</template>
<template v-if="show_settings">
<Modal @close="show_settings = false" content-class="modal-content-max is-overflow-visible">
<Settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
</Modal>
</template>
<template v-if="!simpleMode">
<Shutdown v-if="app_shutdown" /> <Shutdown v-if="app_shutdown" />
<div id="main_container" class="container" v-else> <div id="main_container" class="container" v-else>
<NewVersion v-if="newVersionIsAvailable" /> <NewVersion v-if="newVersionIsAvailable" />
@ -120,13 +132,11 @@
</div> </div>
</div> </div>
</nav> </nav>
<div> <div>
<Settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<NuxtPage v-if="config.is_loaded" /> <NuxtPage v-if="config.is_loaded" />
<Message v-else class="has-background-info-90 has-text-dark mt-5" title="Loading Configuration" <Message v-if="!config.is_loaded" class="has-background-info-90 has-text-dark mt-5"
icon="fas fa-spinner fa-spin"> title="Loading Configuration" icon="fas fa-spinner fa-spin">
<p>Loading application configuration. This usually takes less than a second.</p> <p>Loading application configuration. This usually takes less than a second.</p>
<p v-if="!socket.isConnected" class="mt-2"> <p v-if="!socket.isConnected" class="mt-2">
If this is taking too long, please check that the backend server is running and that the WebSocket If this is taking too long, please check that the backend server is running and that the WebSocket
@ -164,6 +174,7 @@
</div> </div>
</div> </div>
</div> </div>
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -176,6 +187,7 @@ import moment from 'moment'
import type { YTDLPOption } from '~/types/ytdlp' import type { YTDLPOption } from '~/types/ytdlp'
import { useDialog } from '~/composables/useDialog' import { useDialog } from '~/composables/useDialog'
import Dialog from '~/components/Dialog.vue' import Dialog from '~/components/Dialog.vue'
import Simple from '~/components/Simple.vue'
import Shutdown from '~/components/shutdown.vue' import Shutdown from '~/components/shutdown.vue'
import Markdown from '~/components/Markdown.vue' import Markdown from '~/components/Markdown.vue'
@ -191,6 +203,8 @@ const bg_opacity = useStorage('random_bg_opacity', 0.95)
const showMenu = ref(false) const showMenu = ref(false)
const isMobile = useMediaQuery({ maxWidth: 1024 }) const isMobile = useMediaQuery({ maxWidth: 1024 })
const app_shutdown = ref<boolean>(false) const app_shutdown = ref<boolean>(false)
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
const doc = ref<{ file: string }>({ file: '' }) const doc = ref<{ file: string }>({ file: '' })
const applyPreferredColorScheme = (scheme: string) => { const applyPreferredColorScheme = (scheme: string) => {

View file

@ -129,6 +129,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions' import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
import {useConfirm} from '~/composables/useConfirm'
type ConditionItemWithUI = ConditionItem & { raw?: boolean } type ConditionItemWithUI = ConditionItem & { raw?: boolean }
@ -226,7 +227,7 @@ const updateItems = async (newItems: ConditionItem[]): Promise<boolean> => {
} }
const deleteItem = async (cond: ConditionItem): Promise<void> => { const deleteItem = async (cond: ConditionItem): Promise<void> => {
if (true !== (await box.confirm(`Delete '${cond.name}'?`, true))) { if (true !== (await box.confirm(`Delete '${cond.name}'?`))) {
return return
} }

View file

@ -219,6 +219,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { notification, notificationImport } from '~/types/notification' import type { notification, notificationImport } from '~/types/notification'
import {useConfirm} from '~/composables/useConfirm'
const toast = useNotification() const toast = useNotification()
const socket = useSocketStore() const socket = useSocketStore()

View file

@ -204,6 +204,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { Preset } from '~/types/presets' import type { Preset } from '~/types/presets'
import {useConfirm} from '~/composables/useConfirm'
const toast = useNotification() const toast = useNotification()
const config = useConfigStore() const config = useConfigStore()
@ -295,7 +296,7 @@ const updatePresets = async (items: Preset[]): Promise<boolean | undefined> => {
} }
const deleteItem = async (item: Preset) => { const deleteItem = async (item: Preset) => {
if (true !== (await box.confirm(`Delete preset '${item.name}'?`, true))) { if (true !== (await box.confirm(`Delete preset '${item.name}'?`))) {
return return
} }

View file

@ -402,6 +402,7 @@ import moment from 'moment'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser' import { CronExpressionParser } from 'cron-parser'
import Modal from '~/components/Modal.vue' import Modal from '~/components/Modal.vue'
import { useConfirm } from '~/composables/useConfirm'
import TaskInspect from '~/components/TaskInspect.vue' import TaskInspect from '~/components/TaskInspect.vue'
import type { task_item, exported_task, error_response } from '~/types/tasks' import type { task_item, exported_task, error_response } from '~/types/tasks'
@ -595,7 +596,7 @@ const deleteSelected = async () => {
} }
const deleteItem = async (item: task_item) => { const deleteItem = async (item: task_item) => {
if (true !== (await box.confirm(`Delete '${item.name}' task?`, true))) { if (true !== (await box.confirm(`Delete '${item.name}' task?`))) {
return return
} }

View file

@ -24,6 +24,7 @@ export const useConfigStore = defineStore('config', () => {
app_branch: '', app_branch: '',
started: 0, started: 0,
app_env: 'production', app_env: 'production',
simple_mode: false,
}, },
presets: [ presets: [
{ {

View file

@ -26,6 +26,8 @@ type AppConfig = {
browser_control_enabled: boolean browser_control_enabled: boolean
/** Indicates if file logging is enabled */ /** Indicates if file logging is enabled */
file_logging: boolean file_logging: boolean
/** Indicates if the app is in simple mode */
simple_mode: boolean
/** Indicates if the app is running in a native environment (e.g., Electron) */ /** Indicates if the app is running in a native environment (e.g., Electron) */
is_native: boolean is_native: boolean
/** App version in format "1.0.0" */ /** App version in format "1.0.0" */

View file

@ -18,6 +18,8 @@ type StoreItem = {
id: string id: string
/** Title of the item */ /** Title of the item */
title: string title: string
/** Description of the item */
description: string
/** URL of the item */ /** URL of the item */
url: string url: string
/** Preset used for the item */ /** Preset used for the item */

93
uv.lock
View file

@ -875,15 +875,15 @@ wheels = [
[[package]] [[package]]
name = "pyinstaller-hooks-contrib" name = "pyinstaller-hooks-contrib"
version = "2025.8" version = "2025.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "setuptools" }, { name = "setuptools" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/71/d6/e5b378b7d4add8c879295c531309b0320e9c07a70458665d091760ffdc87/pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c", size = 164214 } sdist = { url = "https://files.pythonhosted.org/packages/7d/83/be0f57c0b77b66c33c2283ebd4ea341022b5a743e97c5fb3bebab82b38b9/pyinstaller_hooks_contrib-2025.9.tar.gz", hash = "sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6", size = 165189 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/34/1d973d0dae849683e53fbcda84443ce016f315e6f4dc7605ede4f56a28c3/pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064", size = 442346 }, { url = "https://files.pythonhosted.org/packages/a2/26/23b4cfc77d7f808c69f59070e1e8293a579ec281a547c61562357160b346/pyinstaller_hooks_contrib-2025.9-py3-none-any.whl", hash = "sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038", size = 444283 },
] ]
[[package]] [[package]]
@ -1051,19 +1051,38 @@ wheels = [
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
] ]
[[package]] [[package]]
@ -1239,28 +1258,28 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.13.1" version = "0.13.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987 } sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308 }, { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254 },
{ url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258 }, { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891 },
{ url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554 }, { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588 },
{ url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181 }, { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359 },
{ url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599 }, { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486 },
{ url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178 }, { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203 },
{ url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474 }, { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635 },
{ url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531 }, { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783 },
{ url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267 }, { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322 },
{ url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120 }, { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427 },
{ url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084 }, { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637 },
{ url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105 }, { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025 },
{ url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284 }, { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449 },
{ url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314 }, { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369 },
{ url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360 }, { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644 },
{ url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448 }, { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990 },
{ url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458 }, { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004 },
{ url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893 }, { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437 },
] ]
[[package]] [[package]]
@ -1483,11 +1502,11 @@ wheels = [
[[package]] [[package]]
name = "yt-dlp" name = "yt-dlp"
version = "2025.9.23" version = "2025.9.26"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/77/24a13bbd3190849e7e37e6093aa9648f3b26a836d37ba67e3429ee89ad1d/yt_dlp-2025.9.23.tar.gz", hash = "sha256:9282ad1deadb4c90b2e6d3bcf9f360abf88c5f2e4ba836dad7b51387b086d757", size = 3036855 } sdist = { url = "https://files.pythonhosted.org/packages/58/8f/0daea0feec1ab85e7df85b98ec7cc8c85d706362e80efc5375c7007dc3dc/yt_dlp-2025.9.26.tar.gz", hash = "sha256:c148ae8233ac4ce6c5fbf6f70fcc390f13a00f59da3776d373cf88c5370bda86", size = 3037475 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/b7/723a4061d7efac4bbb86eddef9ceec0fc1e85415c7d665c2e5e1879759bb/yt_dlp-2025.9.23-py3-none-any.whl", hash = "sha256:84e36acf2dfbadb307d734a590dd937923173b13c57cf45a662f40c93e746302", size = 3241497 }, { url = "https://files.pythonhosted.org/packages/35/94/18210c5e6a9d7e622a3b3f4a73dde205f7adf0c46b42b27d0da8c6e5c872/yt_dlp-2025.9.26-py3-none-any.whl", hash = "sha256:36f5fbc153600f759abd48d257231f0e0a547a115ac7ffb05d5b64e5c7fdf8a2", size = 3241906 },
] ]
[[package]] [[package]]