Merge pull request #483 from arabcoders/dev

Feat: add drop file support for cookies field.
This commit is contained in:
Abdulmohsen 2025-11-12 18:59:49 +03:00 committed by GitHub
commit c1b9034ef8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 985 additions and 746 deletions

View file

@ -136,19 +136,23 @@
</div> </div>
<div class="column is-6-tablet is-12-mobile"> <div class="column is-6-tablet is-12-mobile">
<DLInput id="ytdlpCookies" type="text" label="Cookies" v-model="form.cookies" icon="fa-solid fa-cookie" <div class="field">
:disabled="!socket.isConnected || addInProgress" :placeholder="getDefault('cookies', '')"> <label class="label is-unselectable" for="ytdlpCookies">
<template #help> <span class="icon"><i class="fa-solid fa-cookie" /></span>
<span class="help is-bold"> <span>Cookies</span>
<span class="icon"><i class="fa-solid fa-info" /></span> </label>
<span>Use the <NuxtLink target="_blank" <TextDropzone id="ytdlpCookies" v-model="form.cookies" :disabled="!socket.isConnected || addInProgress"
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp"> @error="(msg: string) => toast.error(msg)"
Recommended addon</NuxtLink> by yt-dlp to export cookies. <span class="has-text-danger">The :placeholder="getDefault('cookies', 'Leave empty to use default cookies. Or drag & drop a cookie file here.')" />
cookies MUST be in Netscape HTTP Cookie format.</span> <span class="help is-bold">
</span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use the <NuxtLink target="_blank"
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
Recommended addon</NuxtLink> by yt-dlp to export cookies. <span class="has-text-danger">The
cookies MUST be in Netscape HTTP Cookie format.</span>
</span> </span>
</template> </span>
</DLInput> </div>
</div> </div>
<template v-if="config.dl_fields.length > 0"> <template v-if="config.dl_fields.length > 0">
<div class="column is-6-tablet is-12-mobile" v-for="(fi, index) in sortedDLFields" <div class="column is-6-tablet is-12-mobile" v-for="(fi, index) in sortedDLFields"

View file

@ -134,10 +134,8 @@
<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 output template if non are given with URL. if not set, it will defaults to <span>Use this output template if non are given with URL. if not set, it will defaults to
<code>{{ config.app.output_template }}</code>. <code>{{ config.app.output_template }}</code>. For more information visit <NuxtLink
For more information visit <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template" href="https://github.com/yt-dlp/yt-dlp#output-template" target="_blank">this page</NuxtLink>.
target="_blank">
this page</NuxtLink>.
</span> </span>
</span> </span>
</div> </div>
@ -170,8 +168,9 @@
Cookies Cookies
</label> </label>
<div class="control"> <div class="control">
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" <TextDropzone id="cookies" v-model="form.cookies" :disabled="addInProgress"
placeholder="Leave empty to use default cookies" /> @error="(msg: string) => toast.error(msg)"
placeholder="Leave empty to use default cookies. Or drag & drop a cookie file here." />
</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>

View file

@ -0,0 +1,246 @@
<template>
<div class="file-dropzone is-relative" :class="{ 'is-dragging': isDragging }" @drop.prevent="handleDrop"
@dragover.prevent="handleDragOver" @dragenter.prevent="handleDragEnter" @dragleave.prevent="handleDragLeave"
@click="handleClick">
<textarea ref="textareaRef" class="control textarea is-fullwidth" :class="{ 'is-focused': isDragging }"
:value="model" @input="handleInput" :disabled="disabled" :placeholder="placeholder" :id="id" />
<div v-if="isDragging"
class="dropzone-overlay has-background-success-90 is-flex is-align-items-center is-justify-content-center">
<div class="has-text-centered has-text-dark">
<span class="icon is-large"><i class="fa-solid fa-file-arrow-down fa-3x" /></span>
<p class="mt-3 is-size-5 has-text-weight-bold">Drop file here</p>
</div>
</div>
<input ref="fileInputRef" type="file" :accept="accept" @change="handleFileSelect" style="display: none" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
disabled?: boolean
placeholder?: string
id?: string
accept?: string
maxSize?: number
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
placeholder: '',
id: '',
accept: '',
maxSize: 2 * 1024 * 1024
})
const model = defineModel<string>({ default: '' })
const emit = defineEmits<{ error: [message: string] }>()
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const fileInputRef = ref<HTMLInputElement | null>(null)
const isDragging = ref<boolean>(false)
const dragCounter = ref<number>(0)
const handleInput = (event: Event): void => {
const target = event.target as HTMLTextAreaElement
model.value = target.value
}
const handleDragEnter = (event: DragEvent): void => {
if (props.disabled) {
return
}
dragCounter.value++
if (event.dataTransfer?.types.includes('Files')) {
isDragging.value = true
}
}
const handleDragLeave = (_event: DragEvent): void => {
if (props.disabled) {
return
}
dragCounter.value--
if (0 === dragCounter.value) {
isDragging.value = false
}
}
const handleDragOver = (event: DragEvent): void => {
if (props.disabled) {
return
}
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy'
}
}
const handleDrop = async (event: DragEvent): Promise<void> => {
if (props.disabled) {
return
}
isDragging.value = false
dragCounter.value = 0
const files = event.dataTransfer?.files
if (!files || 0 === files.length) {
return
}
const file = files[0]
if (file) {
await processFile(file)
}
}
const handleClick = (event: MouseEvent): void => {
if (props.disabled) {
return
}
const selection = window.getSelection()
if (selection && selection.toString().length > 0) {
return
}
if (event && event.target === textareaRef.value) {
return
}
}
const handleFileSelect = async (event: Event): Promise<void> => {
const target = event.target as HTMLInputElement
const files = target.files
if (!files || 0 === files.length) {
return
}
const file = files[0]
if (file) {
await processFile(file)
}
if (fileInputRef.value) {
fileInputRef.value.value = ''
}
}
const processFile = async (file: File): Promise<void> => {
try {
if (file.size > props.maxSize) {
const sizeMB = (file.size / 1024 / 1024).toFixed(2)
const maxSizeMB = (props.maxSize / 1024 / 1024).toFixed(2)
const errorMsg = `File too large: ${sizeMB}MB. Maximum allowed size is ${maxSizeMB}MB.`
emit('error', errorMsg)
console.error(errorMsg)
return
}
const isBinary = await checkIfBinary(file)
if (isBinary) {
const errorMsg = 'File appears to be binary. Please provide a text file.'
emit('error', errorMsg)
console.error(errorMsg)
return
}
const text = await readFileAsText(file)
model.value = text
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to read file'
emit('error', errorMsg)
console.error('Failed to read file:', error)
}
}
const checkIfBinary = async (file: File): Promise<boolean> => {
const chunkSize = 8192
const blob = file.slice(0, chunkSize)
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e: ProgressEvent<FileReader>) => {
if (!e.target?.result) {
resolve(false)
return
}
const bytes = new Uint8Array(e.target.result as ArrayBuffer)
let nullBytes = 0
let nonPrintable = 0
for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i]
if (undefined === byte) {
continue
}
if (0 === byte) {
nullBytes++
}
if (9 !== byte && 10 !== byte && 13 !== byte && (byte < 32 || byte > 126)) {
nonPrintable++
}
}
resolve(nullBytes > 0 || (nonPrintable / bytes.length) > 0.3)
}
reader.onerror = () => reject(new Error('Failed to read file for binary check'))
reader.readAsArrayBuffer(blob)
})
}
const readFileAsText = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target?.result) {
resolve(e.target.result as string)
} else {
reject(new Error('Failed to read file'))
}
}
reader.onerror = () => reject(new Error('File reading error'))
reader.readAsText(file)
})
}
</script>
<style>
.file-dropzone {
cursor: text;
}
.dropzone-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 4px;
pointer-events: none;
z-index: 10;
}
.file-dropzone.is-dragging textarea {
box-shadow: --var(--bulma-card-shadow) !important;
border-color: --var(--bulma-success);
}
</style>

View file

@ -3,7 +3,7 @@
<div class="control" style="width:100%;"> <div class="control" style="width:100%;">
<textarea :id="id" ref="textareaRef" v-model="localValue" @input="onInput" @focus="onFocus" @blur="hideList" <textarea :id="id" ref="textareaRef" v-model="localValue" @input="onInput" @focus="onFocus" @blur="hideList"
@keydown="onKeydown" @keyup="updateCaret" @click="updateCaret" @mouseup="updateCaret" @keydown="onKeydown" @keyup="updateCaret" @click="updateCaret" @mouseup="updateCaret"
class="textarea" :placeholder="placeholder" autocomplete="off" class="control is-fullwidth textarea" :placeholder="placeholder" autocomplete="off"
style="width:100%; position:relative; z-index:2;" rows="4" :disabled="disabled" /> style="width:100%; position:relative; z-index:2;" rows="4" :disabled="disabled" />
</div> </div>
<div class="dropdown-menu" role="menu" style="width:100%; z-index:3;"> <div class="dropdown-menu" role="menu" style="width:100%; z-index:3;">

View file

@ -84,8 +84,8 @@
<template> <template>
<div v-if="infoLoaded"> <div v-if="infoLoaded">
<div style="position: relative;"> <div style="position: relative;">
<video class="player" ref="video" :poster="uri(thumbnail)" playsinline controls crossorigin="anonymous" <video class="player" ref="video" :poster="uri(thumbnail)" :width="videoWidth" :height="videoHeight"
preload="auto" autoplay> playsinline controls crossorigin="anonymous" preload="auto" autoplay>
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror" <source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
:type="source.type" /> :type="source.type" />
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label" <track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
@ -275,6 +275,8 @@ const artist = ref('')
const title = ref('') const title = ref('')
const isAudio = ref(false) const isAudio = ref(false)
const hasVideoStream = ref(false) const hasVideoStream = ref(false)
const videoWidth = ref<number | undefined>(undefined)
const videoHeight = ref<number | undefined>(undefined)
const volume = useStorage('player_volume', 1) const volume = useStorage('player_volume', 1)
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox') const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
const infoLoaded = ref(false) const infoLoaded = ref(false)
@ -504,6 +506,15 @@ onMounted(async () => {
hasVideoStream.value = Array.isArray(response.ffprobe?.video) hasVideoStream.value = Array.isArray(response.ffprobe?.video)
&& response.ffprobe.video.some(s => 'video' === s.codec_type) && response.ffprobe.video.some(s => 'video' === s.codec_type)
// Extract video dimensions to prevent layout reflow
if (hasVideoStream.value && response.ffprobe?.video) {
const videoStream = response.ffprobe.video.find(s => 'video' === s.codec_type)
if (videoStream?.width && videoStream?.height) {
videoWidth.value = videoStream.width
videoHeight.value = videoStream.height
}
}
if (!props.item.extras?.is_video && props.item.extras?.is_audio) { if (!props.item.extras?.is_video && props.item.extras?.is_audio) {
isAudio.value = true isAudio.value = true
} else { } else {

View file

@ -154,13 +154,19 @@
<div> <div>
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<NuxtPage v-if="config.is_loaded" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" /> <NuxtPage v-if="config.is_loaded" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
<Message v-if="!config.is_loaded" class="has-background-info-90 has-text-dark mt-5" <Message v-if="!config.is_loaded" class="mt-5" :newStyle="true"
title="Loading Configuration" 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
connection is functional. connection is functional.
</p> </p>
<p v-if="socket.error" class="has-text-danger">
<span class="icon-text">
<span class="icon"><i class="fas fa-triangle-exclamation" /></span>
{{ socket.error }}
</span>
</p>
</Message> </Message>
<Markdown @closeModel="() => doc.file = ''" :file="doc.file" v-if="doc.file" /> <Markdown @closeModel="() => doc.file = ''" :file="doc.file" v-if="doc.file" />
<ClientOnly> <ClientOnly>
@ -182,8 +188,8 @@
- <NuxtLink @click="doc.file = '/api/docs/README.md'">README</NuxtLink> - <NuxtLink @click="doc.file = '/api/docs/README.md'">README</NuxtLink>
- <NuxtLink @click="doc.file = '/api/docs/API.md'">API</NuxtLink> - <NuxtLink @click="doc.file = '/api/docs/API.md'">API</NuxtLink>
- <NuxtLink @click="scrollToTop"> - <NuxtLink @click="scrollToTop">
<span class="icon"><i class="fas fa-arrow-up" /></span> <span class="icon"><i class="fas fa-arrow-up" /></span>
<span>Top</span> <span>Top</span>
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>

View file

@ -13,6 +13,7 @@ export const useSocketStore = defineStore('socket', () => {
const socket = ref<IOSocket | null>(null) const socket = ref<IOSocket | null>(null)
const isConnected = ref<boolean>(false) const isConnected = ref<boolean>(false)
const connectionStatus = ref<connectionStatus>('disconnected') const connectionStatus = ref<connectionStatus>('disconnected')
const error = ref<string | null>(null)
const emit = (event: string, data?: any): any => socket.value?.emit(event, data) const emit = (event: string, data?: any): any => socket.value?.emit(event, data)
const on = (event: string | string[], callback: (...args: any[]) => void, withEvent: boolean = false) => { const on = (event: string | string[], callback: (...args: any[]) => void, withEvent: boolean = false) => {
@ -72,16 +73,26 @@ export const useSocketStore = defineStore('socket', () => {
connectionStatus.value = 'connecting'; connectionStatus.value = 'connecting';
socket.value = io(url, opts) socket.value = io(url, opts)
on("connect_error", (e: any) => console.error("Socket connection error:", e)); on("connect_error", (e: any) => {
isConnected.value = false
if (null === e || undefined === e) {
error.value = 'Connection error: Unknown error';
return;
}
error.value = `Connection error: ${e.type || 'Unknown'}: ${e.message || 'Unknown error'}`;
});
on('connect', () => { on('connect', () => {
isConnected.value = true isConnected.value = true
connectionStatus.value = 'connected'; connectionStatus.value = 'connected';
error.value = null;
}); });
on('disconnect', () => { on('disconnect', () => {
isConnected.value = false isConnected.value = false
connectionStatus.value = 'disconnected'; connectionStatus.value = 'disconnected';
error.value = 'Disconnected from server.';
}); });
on('configuration', stream => { on('configuration', stream => {
@ -100,6 +111,7 @@ export const useSocketStore = defineStore('socket', () => {
config.add('folders', json.data.folders) config.add('folders', json.data.folders)
stateStore.addAll('queue', json.data.queue || {}) stateStore.addAll('queue', json.data.queue || {})
stateStore.addAll('history', json.data.done || {}) stateStore.addAll('history', json.data.done || {})
error.value = null;
}) })
on('item_added', stream => { on('item_added', stream => {
@ -215,5 +227,6 @@ export const useSocketStore = defineStore('socket', () => {
socket, isConnected, socket, isConnected,
getSessionId, getSessionId,
connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>, connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>,
error: readonly(error) as Readonly<Ref<string | null>>,
}; };
}); });

View file

@ -27,15 +27,15 @@
"cronstrue": "^3.9.0", "cronstrue": "^3.9.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"hls.js": "^1.6.14", "hls.js": "^1.6.14",
"marked": "^16.4.1", "marked": "^17.0.0",
"marked-alert": "^2.1.2", "marked-alert": "^2.1.2",
"marked-base-url": "^1.1.7", "marked-base-url": "^1.1.8",
"marked-gfm-heading-id": "^4.1.2", "marked-gfm-heading-id": "^4.1.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"nuxt": "^4.2.0", "nuxt": "^4.2.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"vue": "^3.5.23", "vue": "^3.5.24",
"vue-router": "^4.6.3", "vue-router": "^4.6.3",
"vue-toastification": "2.0.0-rc.5" "vue-toastification": "2.0.0-rc.5"
}, },
@ -57,7 +57,7 @@
"@typescript-eslint/parser": "^8.46.3", "@typescript-eslint/parser": "^8.46.3",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.7", "vitest": "^4.0.8",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.3" "vue-tsc": "^3.1.3"
} }

File diff suppressed because it is too large Load diff

30
uv.lock
View file

@ -237,11 +237,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.10.5" version = "2025.11.12"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519 } sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 }, { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 },
] ]
[[package]] [[package]]
@ -1139,7 +1139,7 @@ wheels = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" version = "9.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@ -1148,21 +1148,21 @@ dependencies = [
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pygments" }, { name = "pygments" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668 },
] ]
[[package]] [[package]]
name = "pytest-asyncio" name = "pytest-asyncio"
version = "1.2.0" version = "1.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pytest" }, { name = "pytest" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119 } sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095 }, { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 },
] ]
[[package]] [[package]]
@ -1649,14 +1649,14 @@ wheels = [
[[package]] [[package]]
name = "wsproto" name = "wsproto"
version = "1.2.0" version = "1.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } sdist = { url = "https://files.pythonhosted.org/packages/91/8d/48e227460422d3f78f52618d8ef7d7a0474c6fcdaddf7f2d1aa25854ea75/wsproto-1.3.1.tar.gz", hash = "sha256:81529992325c28f0d9b86ca66fc973da96eb80ab53410249ce2e502749c7723c", size = 50083 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, { url = "https://files.pythonhosted.org/packages/8f/da/539c2d24b13025e54a86ce3215eb9b6297b023937a087db9ef2a436cc7b4/wsproto-1.3.1-py3-none-any.whl", hash = "sha256:297ce79322989c0d286cc158681641cd18bc7632dfb38cf4054696a89179b993", size = 24402 },
] ]
[[package]] [[package]]
@ -1739,11 +1739,11 @@ wheels = [
[[package]] [[package]]
name = "yt-dlp" name = "yt-dlp"
version = "2025.10.22" version = "2025.11.12"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/08/70/cf4bd6c837ab0a709040888caa70d166aa2dfbb5018d1d5c983bf0b50254/yt_dlp-2025.10.22.tar.gz", hash = "sha256:db2d48133222b1d9508c6de757859c24b5cefb9568cf68ccad85dac20b07f77b", size = 3046863 } sdist = { url = "https://files.pythonhosted.org/packages/cf/41/53ad8c6e74d6627bd598dfbb8ad7c19d5405e438210ad0bbaf1b288387e7/yt_dlp-2025.11.12.tar.gz", hash = "sha256:5f0795a6b8fc57a5c23332d67d6c6acf819a0b46b91a6324bae29414fa97f052", size = 3076928 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/2a/fd184bf97d570841aa86b4aeb84aee93e7957a34059dafd4982157c10bff/yt_dlp-2025.10.22-py3-none-any.whl", hash = "sha256:9c803a9598859f91d0d5bd3337f1506ecb40bbe97f6efbe93bc4461fed344fb2", size = 3248983 }, { url = "https://files.pythonhosted.org/packages/5f/16/fdebbee6473473a1c0576bd165a50e4a70762484d638c1d59fa9074e175b/yt_dlp-2025.11.12-py3-none-any.whl", hash = "sha256:b47af37bbb16b08efebb36825a280ea25a507c051f93bf413a6e4a0e586c6e79", size = 3279151 },
] ]
[[package]] [[package]]