Skip to content

Commit ee4c540

Browse files
author
mithmith
committed
add shorts download
1 parent 1515314 commit ee4c540

7 files changed

Lines changed: 92 additions & 16 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ TG_ADMIN_ID = 1234567890
2121
# MONITOR_HISTORY = 1
2222
# MONITOR_VIDEO_FORMATS = 0
2323
# RUN_TG_BOT = 1
24+
# RUN_TG_BOT_SHORTS_PUBLISH = 0

app/db/data_table.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class Video(Base, table=True):
9292
upload_date: Optional[datetime] = Field(default=None)
9393
defaultaudiolanguage: Optional[str] = Field(default=None)
9494
last_update: datetime = Field(default_factory=lambda: datetime.now().replace(microsecond=0))
95+
tg_post_date: Optional[datetime] = Field(default=None)
9596

9697
channel: Channel = Relationship(back_populates="videos")
9798
thumbnails: List["Thumbnail"] = Relationship(back_populates="video")

app/db/repository.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def update_video(self, video_schema: VideoSchema) -> None:
554554
else:
555555
logger.error(f"Video with ID {video_schema.id} not found in the database.")
556556

557-
def update_video_path(self, video_id: UUID, video_path: Path) -> None:
557+
def update_video_path(self, video_id: str, video_path: Path) -> None:
558558
"""
559559
Updates the file path where the video is stored.
560560
@@ -567,7 +567,7 @@ def update_video_path(self, video_id: UUID, video_path: Path) -> None:
567567
ID exists, its 'video_path' attribute is updated to the new path. The method commits the change
568568
to the database. If the video does not exist, it logs a warning.
569569
"""
570-
video: Video = self._session.query(Video).filter_by(id=video_id).first()
570+
video: Video = self._session.query(Video).filter_by(video_id=video_id).first()
571571
if video:
572572
video.video_path = str(video_path)
573573
self._session.commit()
@@ -680,3 +680,15 @@ def bulk_add_tags(self, tags: list[str]) -> None:
680680
except IntegrityError as e:
681681
self._session.rollback() # Rollback transaction in case of an error
682682
logger.error(f"Error during bulk add tags: {e}")
683+
684+
685+
class YoutubeVideoRepository(BaseRepository[Video]):
686+
model = Video
687+
688+
def update_tg_post_date(self, video_id: str):
689+
"""Обновляет поле tg_post_date для видео после успешной отправки в Telegram."""
690+
with self._session.begin():
691+
video: Video = self._session.query(Video).filter(Video.video_id == video_id).first()
692+
if video:
693+
video.tg_post_date = datetime.now().replace(microsecond=0)
694+
self._session.commit()

app/integrations/ytdlp.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import json
23
import subprocess
34
from pathlib import Path
@@ -11,7 +12,7 @@
1112
from app.db.base import Session
1213
from app.db.data_table import Video
1314
from app.db.repository import YoutubeDataRepository
14-
from app.schema import ChannelInfoSchema, VideoSchema, YTFormatSchema
15+
from app.schema import ChannelInfoSchema, VideoDownloadSchema, VideoSchema, YTFormatSchema
1516

1617

1718
class YTChannelDownloader:
@@ -81,13 +82,21 @@ def _extract_video_list(self) -> tuple[list[VideoSchema], str]:
8182
video_list.append(VideoSchema(**entry))
8283
return video_list, channel_id
8384

84-
def download_video(self, video_id: str, format: str = "bv+ba/b") -> None:
85-
video: Video = self._repository.get_video(video_id)
86-
if video:
87-
video_path = self._construct_video_path(video_id)
88-
command = f'yt-dlp -f "{format}" -o "{video_path}" {video.url}'
89-
subprocess.run(command, shell=True, check=True)
90-
self._repository.update_video_path(video_id, video_path)
85+
@staticmethod
86+
async def download_video(video_info: VideoDownloadSchema, format: str = "best[ext=mp4]") -> None:
87+
command = f'yt-dlp -f "{format}" -o "{video_info.video_download_path}" {video_info.video_url}'
88+
logger.debug("Downloading video: {}".format(video_info.video_url))
89+
logger.debug("Executing command: {}".format(command))
90+
process = await asyncio.create_subprocess_shell(
91+
command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
92+
)
93+
94+
_, stderr = await process.communicate()
95+
96+
if process.returncode == 0:
97+
logger.info(f"Видео скачано: {video_info.video_download_path}")
98+
else:
99+
logger.error(f"Ошибка скачивания видео: {stderr.decode().strip()}")
91100

92101
def download_thumbnail(self, video_id: str) -> None:
93102
video: Video = self._repository.get_video(video_id)

app/schema.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ class NewVideoSchema(BaseModel):
4343
channel_url: str = ""
4444
video_title: str = ""
4545
video_url: str = ""
46+
video_id: str = ""
47+
48+
49+
class VideoDownloadSchema(BaseModel):
50+
file_name: str = ""
51+
video_download_path: str = ""
52+
video_url: str = ""
53+
video_id: str = ""
4654

4755

4856
class ChannelInfoSchema(BaseModel):

app/service/telegram.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from telegram.ext import Application
1111

1212
from app.config import settings
13+
from app.db.base import Session
14+
from app.db.repository import YoutubeVideoRepository
1315
from app.integrations.telegram import get_telegram_handlers
1416
from app.schema import NewVideoSchema
1517

@@ -24,6 +26,7 @@ def __init__(self, bot_token: str, group_id: str, queue: Queue, delay: int = 30)
2426
self._delay = delay # Задержка между отправками сообщений с новыми видео
2527
self._max_retries = 3 # Максимальное количество попыток запуска бота и отправки сообщений
2628
self._retry_delay = 5 # Задержка между неудачными попытками (в секундах)
29+
self._repository = YoutubeVideoRepository(session=Session())
2730
logger.info("Telegram bot is created")
2831

2932
def run(self):
@@ -110,7 +113,9 @@ async def _publish_messages(self, bot: Bot):
110113
)
111114
logger.debug(f"Sending message to {self._group_id}:\n{message}")
112115

113-
await self._send_message_with_retries(bot=bot, chat_id=self._group_id, text=message)
116+
await self._send_message_with_retries(
117+
bot=bot, chat_id=self._group_id, text=message, video_id=video.video_id
118+
)
114119

115120
# Задержка между отправками сообщений
116121
await asyncio.sleep(self._delay)
@@ -121,7 +126,7 @@ async def _publish_messages(self, bot: Bot):
121126
except Exception as e:
122127
logger.error(f"Ошибка при отправке сообщения: {e}")
123128

124-
async def _send_message_with_retries(self, bot: Bot, chat_id: str, text: str):
129+
async def _send_message_with_retries(self, bot: Bot, chat_id: str, text: str, video_id: str):
125130
"""
126131
Отправляет сообщение в Telegram с заданным числом повторных попыток.
127132
@@ -136,6 +141,7 @@ async def _send_message_with_retries(self, bot: Bot, chat_id: str, text: str):
136141
text=text,
137142
parse_mode="Markdown",
138143
)
144+
self._repository.update_tg_post_date(video_id)
139145
logger.info("Сообщение успешно отправлено")
140146
return # Успешная отправка, выходим из функции
141147
except TelegramError as te:

app/service/yt_monitor.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import asyncio
22
from multiprocessing import Process, Queue
3+
from os.path import join
4+
from queue import Empty
35
from typing import Optional
46

57
from loguru import logger
68

9+
from app.config import settings
710
from app.db.base import Session
811
from app.db.data_table import Channel, ChannelHistory, Thumbnail
912
from app.db.repository import YoutubeDataRepository
1013
from app.integrations.ytapi import YTApiClient
1114
from app.integrations.ytdlp import YTChannelDownloader
12-
from app.schema import ChannelAPIInfoSchema, ChannelInfoSchema, NewVideoSchema, VideoSchema
15+
from app.schema import ChannelAPIInfoSchema, ChannelInfoSchema, NewVideoSchema, VideoDownloadSchema, VideoSchema
1316

1417

1518
class YTMonitorService:
@@ -28,6 +31,7 @@ def __init__(
2831
self._history_timeout = history_timeout
2932
self._queue = new_videos_queue # Очередь для обработки новых видео
3033
self._shorts_publish = shorts_publish
34+
self._download_queue = Queue()
3135

3236
def run(
3337
self, monitor_new: bool = True, monitor_history: bool = True, monitor_video_formats: bool = True
@@ -50,6 +54,11 @@ def run(
5054
processes.append(video_formats_process)
5155
video_formats_process.start()
5256

57+
if self._shorts_publish:
58+
shorts_publish_process = Process(target=self._start_async_loop, args=(self._shorts_downloader,))
59+
processes.append(shorts_publish_process)
60+
shorts_publish_process.start()
61+
5362
return processes
5463

5564
def _start_async_loop(self, coro_func, *args, **kwargs):
@@ -96,6 +105,21 @@ async def _update_video_formats(self):
96105
logger.info(f"(FORMATS) Waiting for {self._history_timeout} seconds")
97106
await asyncio.sleep(self._history_timeout)
98107

108+
async def _shorts_downloader(self, delay: int = 5):
109+
while True:
110+
try:
111+
video: VideoDownloadSchema = self._download_queue.get(block=False, timeout=5)
112+
logger.debug(f"video={video}")
113+
await YTChannelDownloader.download_video(video)
114+
# Задержка между скачиванием файлов
115+
await asyncio.sleep(delay)
116+
except Empty:
117+
# Если очередь пуста после таймаута, ничего не делаем и продолжаем ждать
118+
await asyncio.sleep(delay)
119+
continue
120+
except Exception as e:
121+
logger.error(f"Ошибка при отправке сообщения: {e}")
122+
99123
async def _process_channel_videos(self, channel_url: str, process_new: bool = False, process_old: bool = False):
100124
"""Обработка новых и старых видео для канала."""
101125
# Получение информации о канале через yt-dlp
@@ -178,11 +202,26 @@ async def _process_channel_videos(self, channel_url: str, process_new: bool = Fa
178202
channel_url=ytdlp_channel_info.channel_url,
179203
video_title=video.title,
180204
video_url=video.url,
205+
video_id=video.id,
181206
)
182207
) # add video to queue for telegram bot
183-
# elif self._shorts_publish:
184-
# self._queue.put(
185-
# )
208+
elif self._shorts_publish:
209+
video_file_name = (
210+
(ytdlp_channel_info.original_url or ytdlp_channel_info.channel)
211+
+ "_shorts_"
212+
+ video.id
213+
+ ".mp4"
214+
)
215+
self._download_queue.put(
216+
VideoDownloadSchema(
217+
file_name=video_file_name,
218+
video_download_path=join(
219+
settings.storage_path, settings.shorts_download_path, video_file_name
220+
),
221+
video_url=video.url,
222+
video_id=video.id,
223+
)
224+
)
186225
if process_old and old_videos:
187226
self._process_old_videos(old_videos)
188227

0 commit comments

Comments
 (0)