Skip to content

Commit 6e492a5

Browse files
author
mithmith
committed
add fetch_transcript + download_video update
1 parent 89d653b commit 6e492a5

2 files changed

Lines changed: 142 additions & 16 deletions

File tree

app/integrations/ytdlp.py

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import asyncio
2+
import glob
23
import json
34
import locale
5+
import os
6+
import re
47
import subprocess
8+
import tempfile
59
from pathlib import Path
610
from typing import Optional
711

@@ -86,25 +90,32 @@ def _extract_video_list(self) -> tuple[list[VideoSchema], str]:
8690
return video_list, channel_id
8791

8892
@staticmethod
89-
async def download_video(video_info: VideoDownloadSchema, format: str = "best[ext=mp4]") -> None:
90-
if Path(video_info.video_file_download_path).exists():
91-
logger.info(f"Видео уже скачано: {video_info.video_file_download_path}")
93+
async def download_video(
94+
video_info: VideoDownloadSchema,
95+
format: str = "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
96+
ensure_mp4: bool = True,
97+
) -> None:
98+
out_path = Path(video_info.video_file_download_path)
99+
if out_path.exists():
100+
logger.info(f"Видео уже скачано: {out_path}")
92101
return
93102

94-
command = f'yt-dlp -f "{format}" -o "{video_info.video_file_download_path}" {video_info.video_url}'
95-
logger.debug("Downloading video: {}".format(video_info.video_url))
96-
logger.debug("Executing command: {}".format(command))
97-
process = await asyncio.create_subprocess_shell(
103+
postproc_flag = "--recode-video mp4" if ensure_mp4 else "--merge-output-format mp4"
104+
command = f'yt-dlp -f "{format}" {postproc_flag} -o "{out_path}" {video_info.video_url}'
105+
106+
logger.debug(f"Downloading video: {video_info.video_url}")
107+
logger.debug(f"Command: {command}")
108+
109+
proc = await asyncio.create_subprocess_shell(
98110
command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
99111
)
112+
_, stderr = await proc.communicate()
100113

101-
_, stderr = await process.communicate()
102-
103-
if process.returncode == 0:
104-
logger.info(f"Видео скачано: {video_info.video_file_download_path}")
114+
if proc.returncode == 0:
115+
logger.info(f"Видео скачано: {out_path}")
105116
else:
106-
stderr_text = stderr.decode(locale.getpreferredencoding(False), errors="replace").strip()
107-
logger.error(f"Ошибка скачивания видео: {stderr_text}")
117+
err = stderr.decode(locale.getpreferredencoding(False), "replace").strip()
118+
logger.error(f"Ошибка скачивания видео: {err}")
108119

109120
def download_thumbnail(self, video_id: str) -> None:
110121
video: Video = self._repository.get_video(video_id)
@@ -127,7 +138,8 @@ def update_video_formats(self) -> None:
127138
self._repository.add_video_format(format_data, v_id)
128139
logger.debug(f"[{i+1}/{len(video_ids)}] Added video formats for v_id: {v_id}")
129140

130-
def get_video_formats(self, video_id: str) -> list[YTFormatSchema]:
141+
@staticmethod
142+
def get_video_formats(video_id: str) -> list[YTFormatSchema]:
131143
result = subprocess.run(
132144
[
133145
"yt-dlp",
@@ -159,6 +171,120 @@ def get_video_formats(self, video_id: str) -> list[YTFormatSchema]:
159171
logger.error(f"Не удалось декодировать JSON: {e}")
160172
return []
161173

174+
@staticmethod
175+
def fetch_transcript(
176+
video_id: str,
177+
preferred_langs: tuple[str, ...] = ("ru",),
178+
) -> Optional[str]:
179+
"""
180+
Возвращает расшифровку речи YouTube-видео (чистый текст без тайм-кодов).
181+
182+
Порядок поиска языка:
183+
1. Перебор preferred_langs в automatic_captions
184+
2. Первый язык из automatic_captions
185+
3. Перебор preferred_langs в subtitles
186+
4. Первый язык из subtitles
187+
5. None, если субтитров нет
188+
189+
Parameters
190+
----------
191+
video_id : str
192+
Идентификатор ролика (например, '7NSBuTHngK0').
193+
preferred_langs : tuple[str, ...], optional
194+
Коды языков, которые пробуем в первую очередь.
195+
196+
Returns
197+
-------
198+
str | None
199+
Сплошной текст субтитров или None, если ничего не найдено.
200+
"""
201+
video_url = f"https://www.youtube.com/watch?v={video_id}"
202+
203+
# 1. Получаем метаданные ролика
204+
try:
205+
proc = subprocess.run(
206+
["yt-dlp", "-j", video_url],
207+
check=True,
208+
capture_output=True,
209+
text=True,
210+
)
211+
info = json.loads(proc.stdout)
212+
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
213+
logger.error(f"Не удалось получить JSON-метаданные: {e}")
214+
return None
215+
216+
auto_caps: dict[str, list] = info.get("automatic_captions", {}) or {}
217+
man_caps: dict[str, list] = info.get("subtitles", {}) or {}
218+
219+
# 2. Выбираем язык и тип субтитров
220+
lang, use_auto = None, None
221+
222+
for pl in preferred_langs:
223+
if pl in auto_caps:
224+
lang, use_auto = pl, True
225+
break
226+
if lang is None and auto_caps:
227+
lang, use_auto = next(iter(auto_caps)), True
228+
229+
if lang is None:
230+
for pl in preferred_langs:
231+
if pl in man_caps:
232+
lang, use_auto = pl, False
233+
break
234+
if lang is None and man_caps:
235+
lang, use_auto = next(iter(man_caps)), False
236+
237+
if lang is None:
238+
logger.warning(f"У ролика {video_id} нет субтитров")
239+
return None
240+
241+
# 3. Скачиваем json3 и читаем
242+
with tempfile.TemporaryDirectory() as tmpdir:
243+
outtmpl = os.path.join(tmpdir, "subs") # yt-dlp → subs.<lang>.json3
244+
cmd = [
245+
"yt-dlp",
246+
"--skip-download",
247+
"--sub-lang",
248+
lang,
249+
"--sub-format",
250+
"json3",
251+
"-o",
252+
outtmpl,
253+
video_url,
254+
]
255+
cmd.insert(1, "--write-auto-subs" if use_auto else "--write-subs")
256+
257+
try:
258+
subprocess.run(cmd, check=True, capture_output=True, text=True)
259+
except subprocess.CalledProcessError as e:
260+
logger.error(f"Ошибка скачивания субтитров: {e.stderr.strip()}")
261+
return None
262+
263+
pattern = os.path.join(tmpdir, f"subs.{lang}*.json3")
264+
files = glob.glob(pattern)
265+
if not files:
266+
logger.error(f"Файл субтитров не найден ({pattern})")
267+
return None
268+
269+
try:
270+
with open(files[0], encoding="utf-8") as fp:
271+
data = json.load(fp)
272+
except (OSError, json.JSONDecodeError) as e:
273+
logger.error(f"Не удалось прочитать JSON3 файл: {e}")
274+
return None
275+
276+
# 4. Превращаем JSON3 в сплошной текст
277+
if "events" not in data:
278+
logger.error("JSON3 не содержит ключ 'events'")
279+
return None
280+
281+
text_chunks = [seg["utf8"] for ev in data["events"] if "segs" in ev for seg in ev["segs"] if "utf8" in seg]
282+
# Убираем лишние пробелы/переводы строк подряд
283+
transcript = re.sub(r"\s+", " ", " ".join(text_chunks)).strip()
284+
285+
logger.info(f"Расшифровка {video_id} ({lang}) успешно получена ({len(transcript)} символов)")
286+
return transcript
287+
162288
def channel_exist(self, channel_id: str) -> bool:
163289
return bool(self._repository.get_channel_by_id(channel_id))
164290

app/service/yt_monitor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,8 @@ def _process_old_videos(self, old_videos: list[VideoSchema]) -> None:
334334

335335
def _generate_shorts_download_path(self, channel_name: str, video_id: str, format: str = "mp4") -> Path:
336336
video_file_name = f"{channel_name}_{video_id}.{format}"
337-
return (self._short_download_path / video_file_name)
337+
return self._short_download_path / video_file_name
338338

339339
def _generate_videos_download_path(self, channel_name: str, video_id: str, format: str = "mp4") -> Path:
340340
video_file_name = f"{channel_name}_{video_id}.{format}"
341-
return (self._video_download_path / video_file_name)
341+
return self._video_download_path / video_file_name

0 commit comments

Comments
 (0)