исправили баг когда видео из плейлиста
This commit is contained in:
parent
8a21cbe18a
commit
09347b45ec
2 changed files with 76 additions and 8 deletions
|
|
@ -3,6 +3,8 @@ import asyncio
|
|||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from collections import deque
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
|
@ -11,13 +13,14 @@ from app.config import Config
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sanitize_filename(filename: str, max_length: int = 150) -> str:
|
||||
def sanitize_filename(filename: str, max_length: int = 150, max_bytes: int = 120) -> str:
|
||||
"""
|
||||
Очистка имени файла от запрещённых символов.
|
||||
|
||||
Args:
|
||||
filename: Исходное имя файла
|
||||
max_length: Максимальная длина имени файла
|
||||
max_bytes: Максимальная длина имени файла в байтах (UTF-8)
|
||||
|
||||
Returns:
|
||||
Безопасное имя файла
|
||||
|
|
@ -34,6 +37,18 @@ def sanitize_filename(filename: str, max_length: int = 150) -> str:
|
|||
# Ограничиваем длину
|
||||
if len(sanitized) > max_length:
|
||||
sanitized = sanitized[:max_length]
|
||||
|
||||
# Ограничиваем длину в байтах (на случай UTF-8 и лимита FS ~255 байт)
|
||||
if len(sanitized.encode('utf-8')) > max_bytes:
|
||||
trimmed = []
|
||||
total_bytes = 0
|
||||
for ch in sanitized:
|
||||
ch_bytes = len(ch.encode('utf-8'))
|
||||
if total_bytes + ch_bytes > max_bytes:
|
||||
break
|
||||
trimmed.append(ch)
|
||||
total_bytes += ch_bytes
|
||||
sanitized = ''.join(trimmed)
|
||||
|
||||
# Если имя пустое, используем дефолтное
|
||||
if not sanitized:
|
||||
|
|
@ -51,6 +66,41 @@ def is_youtube_url(url: str) -> bool:
|
|||
return any(re.search(pattern, url) for pattern in patterns)
|
||||
|
||||
|
||||
def normalize_youtube_url(url: str) -> str:
|
||||
"""Свести URL к одиночному видео (убрать list/index и т.п.)."""
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
host = parsed.netloc.lower()
|
||||
path = parsed.path or ""
|
||||
|
||||
# youtu.be/<id>
|
||||
if "youtu.be" in host:
|
||||
video_id = path.strip("/").split("/")[0]
|
||||
if video_id:
|
||||
qs = parse_qs(parsed.query)
|
||||
t = qs.get("t") or qs.get("start")
|
||||
t_suffix = f"&t={t[0]}" if t else ""
|
||||
return f"https://www.youtube.com/watch?v={video_id}{t_suffix}"
|
||||
return url
|
||||
|
||||
# youtube.com/watch?v=<id> or /shorts/<id>
|
||||
qs = parse_qs(parsed.query)
|
||||
video_id = None
|
||||
if "v" in qs and qs["v"]:
|
||||
video_id = qs["v"][0]
|
||||
elif path.startswith("/shorts/"):
|
||||
video_id = path.split("/")[2] if len(path.split("/")) > 2 else None
|
||||
|
||||
if video_id:
|
||||
t = qs.get("t") or qs.get("start")
|
||||
t_suffix = f"&t={t[0]}" if t else ""
|
||||
return f"https://www.youtube.com/watch?v={video_id}{t_suffix}"
|
||||
except Exception:
|
||||
return url
|
||||
|
||||
return url
|
||||
|
||||
|
||||
async def get_video_title(url: str, config: Optional[Config] = None) -> Optional[str]:
|
||||
"""
|
||||
Получить название видео через yt-dlp.
|
||||
|
|
@ -68,6 +118,7 @@ async def get_video_title(url: str, config: Optional[Config] = None) -> Optional
|
|||
'--skip-download',
|
||||
'--get-title',
|
||||
'--no-warnings',
|
||||
'--no-playlist',
|
||||
url
|
||||
]
|
||||
if config:
|
||||
|
|
@ -147,6 +198,7 @@ async def download_and_convert(
|
|||
'--no-warnings',
|
||||
'--progress',
|
||||
'--newline',
|
||||
'--no-playlist',
|
||||
url
|
||||
]
|
||||
if config:
|
||||
|
|
@ -167,20 +219,24 @@ async def download_and_convert(
|
|||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
async def _log_stream(stream, level: str):
|
||||
stderr_tail = deque(maxlen=12)
|
||||
stdout_tail = deque(maxlen=12)
|
||||
|
||||
async def _log_stream(stream, level: str, tail: deque[str]):
|
||||
while True:
|
||||
line = await stream.readline()
|
||||
if not line:
|
||||
break
|
||||
text = line.decode('utf-8', errors='ignore').strip()
|
||||
if text:
|
||||
tail.append(text)
|
||||
if level == "info":
|
||||
logger.info(f"yt-dlp: {text}")
|
||||
else:
|
||||
logger.warning(f"yt-dlp: {text}")
|
||||
|
||||
stderr_task = asyncio.create_task(_log_stream(process.stderr, "info"))
|
||||
stdout_task = asyncio.create_task(_log_stream(process.stdout, "info"))
|
||||
stderr_task = asyncio.create_task(_log_stream(process.stderr, "warn", stderr_tail))
|
||||
stdout_task = asyncio.create_task(_log_stream(process.stdout, "info", stdout_tail))
|
||||
|
||||
await process.wait()
|
||||
await stderr_task
|
||||
|
|
@ -188,6 +244,10 @@ async def download_and_convert(
|
|||
|
||||
if process.returncode != 0:
|
||||
logger.error("yt-dlp failed")
|
||||
tail_lines = list(stderr_tail) + list(stdout_tail)
|
||||
if tail_lines:
|
||||
tail_text = "\n".join(tail_lines[-12:])
|
||||
raise Exception(f"Ошибка скачивания: yt-dlp завершился с ошибкой\n{tail_text}")
|
||||
raise Exception("Ошибка скачивания: yt-dlp завершился с ошибкой")
|
||||
|
||||
# Находим скачанный файл (yt-dlp создаст файл с расширением)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue