исправили баг когда видео из плейлиста
This commit is contained in:
parent
8a21cbe18a
commit
09347b45ec
2 changed files with 76 additions and 8 deletions
|
|
@ -17,7 +17,13 @@ from telegram.request import HTTPXRequest
|
||||||
|
|
||||||
from app.config import Config
|
from app.config import Config
|
||||||
from app.queue_manager import QueueManager, Task
|
from app.queue_manager import QueueManager, Task
|
||||||
from app.youtube_downloader import is_youtube_url, get_video_title, download_and_convert, sanitize_filename
|
from app.youtube_downloader import (
|
||||||
|
is_youtube_url,
|
||||||
|
get_video_title,
|
||||||
|
download_and_convert,
|
||||||
|
sanitize_filename,
|
||||||
|
normalize_youtube_url,
|
||||||
|
)
|
||||||
from app.admin_manager import AdminManager
|
from app.admin_manager import AdminManager
|
||||||
from app.statistics import Statistics
|
from app.statistics import Statistics
|
||||||
|
|
||||||
|
|
@ -47,14 +53,16 @@ async def process_task(task: Task, config: Config, admin_manager: AdminManager,
|
||||||
Returns:
|
Returns:
|
||||||
Путь к созданному MP3 файлу
|
Путь к созданному MP3 файлу
|
||||||
"""
|
"""
|
||||||
|
normalized_url = normalize_youtube_url(task.url)
|
||||||
|
|
||||||
# Получаем название видео
|
# Получаем название видео
|
||||||
title = await get_video_title(task.url, config=config)
|
title = await get_video_title(normalized_url, config=config)
|
||||||
|
|
||||||
# Если название не получено, запрашиваем у пользователя
|
# Если название не получено, запрашиваем у пользователя
|
||||||
if not title:
|
if not title:
|
||||||
# Сохраняем информацию о запросе
|
# Сохраняем информацию о запросе
|
||||||
output_path = config.workdir / f"task_{task.task_id}"
|
output_path = config.workdir / f"task_{task.task_id}"
|
||||||
pending_filename_requests[task.user_id] = (task.url, output_path)
|
pending_filename_requests[task.user_id] = (normalized_url, output_path)
|
||||||
|
|
||||||
# Отправляем запрос пользователю
|
# Отправляем запрос пользователю
|
||||||
status_callback = task.callback
|
status_callback = task.callback
|
||||||
|
|
@ -79,7 +87,7 @@ async def process_task(task: Task, config: Config, admin_manager: AdminManager,
|
||||||
output_path = config.workdir / f"task_{task.task_id}_{safe_title}"
|
output_path = config.workdir / f"task_{task.task_id}_{safe_title}"
|
||||||
|
|
||||||
# Скачиваем и конвертируем
|
# Скачиваем и конвертируем
|
||||||
mp3_path = await download_and_convert(task.url, output_path, custom_title=safe_title, config=config)
|
mp3_path = await download_and_convert(normalized_url, output_path, custom_title=safe_title, config=config)
|
||||||
|
|
||||||
return str(mp3_path)
|
return str(mp3_path)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from collections import deque
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -11,13 +13,14 @@ from app.config import Config
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
Args:
|
||||||
filename: Исходное имя файла
|
filename: Исходное имя файла
|
||||||
max_length: Максимальная длина имени файла
|
max_length: Максимальная длина имени файла
|
||||||
|
max_bytes: Максимальная длина имени файла в байтах (UTF-8)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Безопасное имя файла
|
Безопасное имя файла
|
||||||
|
|
@ -34,6 +37,18 @@ def sanitize_filename(filename: str, max_length: int = 150) -> str:
|
||||||
# Ограничиваем длину
|
# Ограничиваем длину
|
||||||
if len(sanitized) > max_length:
|
if len(sanitized) > max_length:
|
||||||
sanitized = 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:
|
if not sanitized:
|
||||||
|
|
@ -51,6 +66,41 @@ def is_youtube_url(url: str) -> bool:
|
||||||
return any(re.search(pattern, url) for pattern in patterns)
|
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]:
|
async def get_video_title(url: str, config: Optional[Config] = None) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Получить название видео через yt-dlp.
|
Получить название видео через yt-dlp.
|
||||||
|
|
@ -68,6 +118,7 @@ async def get_video_title(url: str, config: Optional[Config] = None) -> Optional
|
||||||
'--skip-download',
|
'--skip-download',
|
||||||
'--get-title',
|
'--get-title',
|
||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
|
'--no-playlist',
|
||||||
url
|
url
|
||||||
]
|
]
|
||||||
if config:
|
if config:
|
||||||
|
|
@ -147,6 +198,7 @@ async def download_and_convert(
|
||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
'--progress',
|
'--progress',
|
||||||
'--newline',
|
'--newline',
|
||||||
|
'--no-playlist',
|
||||||
url
|
url
|
||||||
]
|
]
|
||||||
if config:
|
if config:
|
||||||
|
|
@ -167,20 +219,24 @@ async def download_and_convert(
|
||||||
stderr=asyncio.subprocess.PIPE
|
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:
|
while True:
|
||||||
line = await stream.readline()
|
line = await stream.readline()
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
text = line.decode('utf-8', errors='ignore').strip()
|
text = line.decode('utf-8', errors='ignore').strip()
|
||||||
if text:
|
if text:
|
||||||
|
tail.append(text)
|
||||||
if level == "info":
|
if level == "info":
|
||||||
logger.info(f"yt-dlp: {text}")
|
logger.info(f"yt-dlp: {text}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"yt-dlp: {text}")
|
logger.warning(f"yt-dlp: {text}")
|
||||||
|
|
||||||
stderr_task = asyncio.create_task(_log_stream(process.stderr, "info"))
|
stderr_task = asyncio.create_task(_log_stream(process.stderr, "warn", stderr_tail))
|
||||||
stdout_task = asyncio.create_task(_log_stream(process.stdout, "info"))
|
stdout_task = asyncio.create_task(_log_stream(process.stdout, "info", stdout_tail))
|
||||||
|
|
||||||
await process.wait()
|
await process.wait()
|
||||||
await stderr_task
|
await stderr_task
|
||||||
|
|
@ -188,6 +244,10 @@ async def download_and_convert(
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
logger.error("yt-dlp failed")
|
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 завершился с ошибкой")
|
raise Exception("Ошибка скачивания: yt-dlp завершился с ошибкой")
|
||||||
|
|
||||||
# Находим скачанный файл (yt-dlp создаст файл с расширением)
|
# Находим скачанный файл (yt-dlp создаст файл с расширением)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue