Фоновые задачи: плейлисты, длинные лекции, вкладка настроек
- таблица jobs + очередь с воркером: POST /jobs сразу отвечает, элементы
плейлиста обрабатываются последовательно, есть отмена и живой прогресс
(скачивание %, минуты распознанного Vosk-аудио)
- expand_url: плейлист раскрывается в список видео
- keep_video=false теперь качает только аудио (для 4-5ч лекций)
- настройки (LLM-ключ, модель, длина выжимки, хранение видео, путь Vosk)
хранятся в Postgres: переживают перезапуск и перезагрузку страницы
- длина выжимки прокидывается в LLM-промпт ({n} предложений)
- UI: вкладки «Главная»/«Настройки», карточка задач с автообновлением
каждые 3 сек, прогресс-бар, отмена, библиотека обновляется по мере
готовности элементов
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,44 @@ import yt_dlp
|
||||
logger = logging.getLogger("youtube")
|
||||
|
||||
|
||||
_VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".m4a", ".mp3", ".mov"}
|
||||
_VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".m4a", ".mp3", ".mov", ".opus", ".ogg", ".aac"}
|
||||
|
||||
|
||||
def expand_url(url: str) -> dict:
|
||||
"""Определяет, видео это или плейлист, и возвращает список элементов.
|
||||
|
||||
Returns: {"kind": "video"|"playlist", "title": str,
|
||||
"entries": [{"url": str, "title": str}, ...]}
|
||||
"""
|
||||
opts = {
|
||||
"extract_flat": "in_playlist",
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
}
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
info = ydl.extract_info(url, download=False)
|
||||
|
||||
if info.get("_type") == "playlist":
|
||||
entries = []
|
||||
for e in info.get("entries") or []:
|
||||
if not e:
|
||||
continue
|
||||
u = e.get("url") or ""
|
||||
if u and not u.startswith("http"):
|
||||
u = f"https://www.youtube.com/watch?v={u}"
|
||||
if not u and e.get("id"):
|
||||
u = f"https://www.youtube.com/watch?v={e['id']}"
|
||||
if u:
|
||||
entries.append({"url": u, "title": e.get("title") or u})
|
||||
return {
|
||||
"kind": "playlist",
|
||||
"title": info.get("title") or url,
|
||||
"entries": entries,
|
||||
}
|
||||
|
||||
title = info.get("title") or url
|
||||
return {"kind": "video", "title": title, "entries": [{"url": url, "title": title}]}
|
||||
|
||||
|
||||
def download_video_with_subs(
|
||||
@@ -20,9 +57,15 @@ def download_video_with_subs(
|
||||
video_id: str,
|
||||
video_dir: Path,
|
||||
langs: Optional[list[str]] = None,
|
||||
audio_only: bool = False,
|
||||
on_progress=None,
|
||||
) -> dict:
|
||||
"""Download a single video, also requesting subtitles for the given languages.
|
||||
|
||||
audio_only: качает только аудиодорожку (для длинных лекций, когда видео
|
||||
не сохраняется — в разы быстрее и меньше по объёму).
|
||||
on_progress: callback(percent: int), вызывается с шагом ~5%.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"title": str,
|
||||
@@ -40,11 +83,34 @@ def download_video_with_subs(
|
||||
|
||||
base_opts = {
|
||||
"outtmpl": outtmpl,
|
||||
"format": "bv*+ba/b",
|
||||
"merge_output_format": "mp4",
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"noplaylist": True, # ссылка с list= в задаче на одно видео не тянет весь плейлист
|
||||
}
|
||||
if audio_only:
|
||||
base_opts["format"] = "ba/b"
|
||||
else:
|
||||
base_opts["format"] = "bv*+ba/b"
|
||||
base_opts["merge_output_format"] = "mp4"
|
||||
|
||||
if on_progress is not None:
|
||||
state = {"last": -5}
|
||||
|
||||
def _hook(dd: dict):
|
||||
if dd.get("status") != "downloading":
|
||||
return
|
||||
total = dd.get("total_bytes") or dd.get("total_bytes_estimate")
|
||||
if not total:
|
||||
return
|
||||
pct = int(dd.get("downloaded_bytes", 0) * 100 / total)
|
||||
if pct >= state["last"] + 5:
|
||||
state["last"] = pct
|
||||
try:
|
||||
on_progress(pct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base_opts["progress_hooks"] = [_hook]
|
||||
sub_opts = {
|
||||
**base_opts,
|
||||
"writesubtitles": True,
|
||||
|
||||
Reference in New Issue
Block a user