Merge pull request #483 from arabcoders/dev
Feat: add drop file support for cookies field.
This commit is contained in:
commit
c1b9034ef8
10 changed files with 985 additions and 746 deletions
|
|
@ -136,19 +136,23 @@
|
|||
</div>
|
||||
|
||||
<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"
|
||||
:disabled="!socket.isConnected || addInProgress" :placeholder="getDefault('cookies', '')">
|
||||
<template #help>
|
||||
<span class="help is-bold">
|
||||
<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>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="ytdlpCookies">
|
||||
<span class="icon"><i class="fa-solid fa-cookie" /></span>
|
||||
<span>Cookies</span>
|
||||
</label>
|
||||
<TextDropzone id="ytdlpCookies" v-model="form.cookies" :disabled="!socket.isConnected || addInProgress"
|
||||
@error="(msg: string) => toast.error(msg)"
|
||||
:placeholder="getDefault('cookies', 'Leave empty to use default cookies. Or drag & drop a cookie file here.')" />
|
||||
<span class="help is-bold">
|
||||
<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>
|
||||
</template>
|
||||
</DLInput>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="config.dl_fields.length > 0">
|
||||
<div class="column is-6-tablet is-12-mobile" v-for="(fi, index) in sortedDLFields"
|
||||
|
|
|
|||
|
|
@ -134,10 +134,8 @@
|
|||
<span class="help is-bold">
|
||||
<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
|
||||
<code>{{ config.app.output_template }}</code>.
|
||||
For more information visit <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
||||
target="_blank">
|
||||
this page</NuxtLink>.
|
||||
<code>{{ config.app.output_template }}</code>. For more information visit <NuxtLink
|
||||
href="https://github.com/yt-dlp/yt-dlp#output-template" target="_blank">this page</NuxtLink>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -170,8 +168,9 @@
|
|||
Cookies
|
||||
</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress"
|
||||
placeholder="Leave empty to use default cookies" />
|
||||
<TextDropzone id="cookies" v-model="form.cookies" :disabled="addInProgress"
|
||||
@error="(msg: string) => toast.error(msg)"
|
||||
placeholder="Leave empty to use default cookies. Or drag & drop a cookie file here." />
|
||||
</div>
|
||||
<span class="help is-bold">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
|
|
|
|||
246
ui/app/components/TextDropzone.vue
Normal file
246
ui/app/components/TextDropzone.vue
Normal 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>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="control" style="width:100%;">
|
||||
<textarea :id="id" ref="textareaRef" v-model="localValue" @input="onInput" @focus="onFocus" @blur="hideList"
|
||||
@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" />
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu" style="width:100%; z-index:3;">
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@
|
|||
<template>
|
||||
<div v-if="infoLoaded">
|
||||
<div style="position: relative;">
|
||||
<video class="player" ref="video" :poster="uri(thumbnail)" playsinline controls crossorigin="anonymous"
|
||||
preload="auto" autoplay>
|
||||
<video class="player" ref="video" :poster="uri(thumbnail)" :width="videoWidth" :height="videoHeight"
|
||||
playsinline controls crossorigin="anonymous" preload="auto" autoplay>
|
||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||
:type="source.type" />
|
||||
<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 isAudio = 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 notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
|
||||
const infoLoaded = ref(false)
|
||||
|
|
@ -504,6 +506,15 @@ onMounted(async () => {
|
|||
hasVideoStream.value = Array.isArray(response.ffprobe?.video)
|
||||
&& 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) {
|
||||
isAudio.value = true
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -154,13 +154,19 @@
|
|||
<div>
|
||||
<NuxtLoadingIndicator />
|
||||
<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">
|
||||
<p>Loading application configuration. This usually takes less than a second.</p>
|
||||
<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
|
||||
connection is functional.
|
||||
</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>
|
||||
<Markdown @closeModel="() => doc.file = ''" :file="doc.file" v-if="doc.file" />
|
||||
<ClientOnly>
|
||||
|
|
@ -182,8 +188,8 @@
|
|||
- <NuxtLink @click="doc.file = '/api/docs/README.md'">README</NuxtLink>
|
||||
- <NuxtLink @click="doc.file = '/api/docs/API.md'">API</NuxtLink>
|
||||
- <NuxtLink @click="scrollToTop">
|
||||
<span class="icon"><i class="fas fa-arrow-up" /></span>
|
||||
<span>Top</span>
|
||||
<span class="icon"><i class="fas fa-arrow-up" /></span>
|
||||
<span>Top</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
const socket = ref<IOSocket | null>(null)
|
||||
const isConnected = ref<boolean>(false)
|
||||
const connectionStatus = ref<connectionStatus>('disconnected')
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const emit = (event: string, data?: any): any => socket.value?.emit(event, data)
|
||||
const on = (event: string | string[], callback: (...args: any[]) => void, withEvent: boolean = false) => {
|
||||
|
|
@ -72,16 +73,26 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
connectionStatus.value = 'connecting';
|
||||
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', () => {
|
||||
isConnected.value = true
|
||||
connectionStatus.value = 'connected';
|
||||
error.value = null;
|
||||
});
|
||||
|
||||
on('disconnect', () => {
|
||||
isConnected.value = false
|
||||
connectionStatus.value = 'disconnected';
|
||||
error.value = 'Disconnected from server.';
|
||||
});
|
||||
|
||||
on('configuration', stream => {
|
||||
|
|
@ -100,6 +111,7 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
config.add('folders', json.data.folders)
|
||||
stateStore.addAll('queue', json.data.queue || {})
|
||||
stateStore.addAll('history', json.data.done || {})
|
||||
error.value = null;
|
||||
})
|
||||
|
||||
on('item_added', stream => {
|
||||
|
|
@ -215,5 +227,6 @@ export const useSocketStore = defineStore('socket', () => {
|
|||
socket, isConnected,
|
||||
getSessionId,
|
||||
connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>,
|
||||
error: readonly(error) as Readonly<Ref<string | null>>,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,15 +27,15 @@
|
|||
"cronstrue": "^3.9.0",
|
||||
"floating-vue": "^5.2.2",
|
||||
"hls.js": "^1.6.14",
|
||||
"marked": "^16.4.1",
|
||||
"marked": "^17.0.0",
|
||||
"marked-alert": "^2.1.2",
|
||||
"marked-base-url": "^1.1.7",
|
||||
"marked-gfm-heading-id": "^4.1.2",
|
||||
"marked-base-url": "^1.1.8",
|
||||
"marked-gfm-heading-id": "^4.1.3",
|
||||
"moment": "^2.30.1",
|
||||
"nuxt": "^4.2.0",
|
||||
"nuxt": "^4.2.1",
|
||||
"pinia": "^3.0.4",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.5.23",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3",
|
||||
"vue-toastification": "2.0.0-rc.5"
|
||||
},
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.7",
|
||||
"vitest": "^4.0.8",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.3"
|
||||
}
|
||||
|
|
|
|||
1360
ui/pnpm-lock.yaml
1360
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
30
uv.lock
30
uv.lock
|
|
@ -237,11 +237,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
version = "2025.11.12"
|
||||
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 = [
|
||||
{ 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]]
|
||||
|
|
@ -1139,7 +1139,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
version = "9.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
|
|
@ -1148,21 +1148,21 @@ dependencies = [
|
|||
{ name = "pluggy" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
name = "pytest-asyncio"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
|
@ -1649,14 +1649,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "wsproto"
|
||||
version = "1.2.0"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
|
@ -1739,11 +1739,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2025.10.22"
|
||||
version = "2025.11.12"
|
||||
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 = [
|
||||
{ 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]]
|
||||
|
|
|
|||
Loading…
Reference in a new issue