Version 1.7

This commit is contained in:
Taras Terletsky 2024-05-30 22:39:51 +03:00
parent b2cbbaa184
commit 5763194699
17 changed files with 111 additions and 58 deletions

View file

@ -2,7 +2,7 @@
Simple and reliable self-hosted Video Download Telegram Bot. Simple and reliable self-hosted Video Download Telegram Bot.
Version: 1.6. [Release details](RELEASES.md). Version: 1.7. [Release details](RELEASES.md).
![frames](.assets/download_success.png) ![frames](.assets/download_success.png)

View file

@ -1,3 +1,22 @@
## Release 1.7
Release date: May 30, 2024
## New Features
- Choose `yt-dlp` release channel between `NIGHTLY`, `STABLE` or `MASTER`. Default is `NIGHTLY`.
## Important
- Update your `app_bot/config.yaml` with new `release_channel: "NIGHTLY"` line in `ytdlp` section
- `app_worker/requirements.txt` includes `NIGHTLY` `yt-dlp` package to install.
## Misc
N/A
---
## Release 1.6 ## Release 1.6
Release date: April 25, 2024 Release date: April 25, 2024

View file

@ -1,6 +1,9 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from yt_shared.clients.github import YtdlpGithubClient
from yt_shared.db.session import get_db from yt_shared.db.session import get_db
from yt_shared.enums import YtdlpReleaseChannelType
from yt_shared.repositories.ytdlp import YtdlpRepository
from yt_shared.ytdlp.version_checker import YtdlpVersionChecker from yt_shared.ytdlp.version_checker import YtdlpVersionChecker
from api.api.api_v1.schemas.ytdlp import YTDLPLatestVersion from api.api.api_v1.schemas.ytdlp import YTDLPLatestVersion
@ -9,8 +12,12 @@ router = APIRouter()
@router.get('/', response_model_by_alias=False) @router.get('/', response_model_by_alias=False)
async def yt_dlp_version(db: AsyncSession = Depends(get_db)) -> YTDLPLatestVersion: async def yt_dlp_version(
ctx = await YtdlpVersionChecker().get_version_context(db) release_channel: YtdlpReleaseChannelType, db: AsyncSession = Depends(get_db)
) -> YTDLPLatestVersion:
ctx = await YtdlpVersionChecker(
client=YtdlpGithubClient(release_channel), repository=YtdlpRepository(db)
).get_version_context()
return YTDLPLatestVersion( return YTDLPLatestVersion(
current=ctx.current, latest=ctx.latest, need_upgrade=ctx.has_new_version current=ctx.current, latest=ctx.latest, need_upgrade=ctx.has_new_version
) )

View file

@ -6,7 +6,7 @@ from pyrogram import Client
from pyrogram.enums import ParseMode from pyrogram.enums import ParseMode
from pyrogram.errors import RPCError from pyrogram.errors import RPCError
from bot.core.schema import ConfigSchema, UserSchema from bot.core.schemas import ConfigSchema, UserSchema
from bot.core.utils import bold from bot.core.utils import bold

View file

@ -9,7 +9,7 @@ from pydantic import ValidationError
from yt_shared.config import Settings from yt_shared.config import Settings
from bot.core.exceptions import ConfigError from bot.core.exceptions import ConfigError
from bot.core.schema import ConfigSchema from bot.core.schemas import ConfigSchema
class ConfigLoader: class ConfigLoader:

View file

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from yt_shared.enums import TaskSource, TelegramChatType from yt_shared.enums import TaskSource, TelegramChatType
from yt_shared.schemas.base_rabbit import BaseRabbitDownloadPayload from yt_shared.schemas.base_rabbit import BaseRabbitDownloadPayload
from bot.core.schema import AnonymousUserSchema, UserSchema from bot.core.schemas import AnonymousUserSchema, UserSchema
if TYPE_CHECKING: if TYPE_CHECKING:
from bot.bot import VideoBotClient from bot.bot import VideoBotClient

View file

@ -2,7 +2,7 @@ from abc import ABC
from pydantic import Field, PositiveInt, StringConstraints, field_validator from pydantic import Field, PositiveInt, StringConstraints, field_validator
from typing_extensions import Annotated from typing_extensions import Annotated
from yt_shared.enums import DownMediaType from yt_shared.enums import DownMediaType, YtdlpReleaseChannelType
from yt_shared.schemas.base import StrictBaseConfigModel from yt_shared.schemas.base import StrictBaseConfigModel
_LANG_CODE_LEN = 2 _LANG_CODE_LEN = 2
@ -72,6 +72,7 @@ class YtdlpSchema(StrictBaseConfigModel):
version_check_enabled: bool version_check_enabled: bool
version_check_interval: PositiveInt version_check_interval: PositiveInt
notify_users_on_new_version: bool notify_users_on_new_version: bool
release_channel: Annotated[YtdlpReleaseChannelType, Field(strict=False)]
class ConfigSchema(StrictBaseConfigModel): class ConfigSchema(StrictBaseConfigModel):

View file

@ -10,7 +10,7 @@ from yt_shared.rabbit.publisher import RmqPublisher
from yt_shared.schemas.media import InbMediaPayload from yt_shared.schemas.media import InbMediaPayload
from yt_shared.schemas.url import URL from yt_shared.schemas.url import URL
from bot.core.schema import UserSchema from bot.core.schemas import UserSchema
from bot.core.utils import can_remove_url_params from bot.core.utils import can_remove_url_params

View file

@ -19,7 +19,7 @@ from yt_shared.utils.tasks.abstract import AbstractTask
from yt_shared.utils.tasks.tasks import create_task from yt_shared.utils.tasks.tasks import create_task
from bot.core.config.config import get_main_config, settings from bot.core.config.config import get_main_config, settings
from bot.core.schema import AnonymousUserSchema, UserSchema, VideoCaptionSchema from bot.core.schemas import AnonymousUserSchema, UserSchema, VideoCaptionSchema
from bot.core.utils import bold, is_user_upload_silent from bot.core.utils import bold, is_user_upload_silent
if TYPE_CHECKING: if TYPE_CHECKING:

View file

@ -2,8 +2,10 @@ import asyncio
import datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from yt_shared.clients.github import YtdlpGithubClient
from yt_shared.db.session import get_db from yt_shared.db.session import get_db
from yt_shared.emoji import INFORMATION_EMOJI from yt_shared.emoji import INFORMATION_EMOJI
from yt_shared.repositories.ytdlp import YtdlpRepository
from yt_shared.schemas.ytdlp import VersionContext from yt_shared.schemas.ytdlp import VersionContext
from yt_shared.utils.tasks.abstract import AbstractTask from yt_shared.utils.tasks.abstract import AbstractTask
from yt_shared.ytdlp.version_checker import YtdlpVersionChecker from yt_shared.ytdlp.version_checker import YtdlpVersionChecker
@ -12,53 +14,60 @@ from bot.core.config.config import get_main_config
from bot.core.utils import bold, code from bot.core.utils import bold, code
if TYPE_CHECKING: if TYPE_CHECKING:
from bot.bot import VideoBotClient from bot.bot.client import VideoBotClient
class YtdlpNewVersionNotifyTask(AbstractTask): class YtdlpNewVersionNotifyTask(AbstractTask):
def __init__(self, bot: 'VideoBotClient') -> None: def __init__(self, bot: 'VideoBotClient') -> None:
super().__init__() super().__init__()
self._bot = bot self._bot = bot
self._version_checker = YtdlpVersionChecker()
self._startup_message_sent = False self._startup_message_sent = False
self._ytdlp_conf = get_main_config().ytdlp
ytdlp_conf = get_main_config().ytdlp
self._version_check_enabled = ytdlp_conf.version_check_enabled
self._version_check_interval = ytdlp_conf.version_check_interval
self._notify_users_on_new_version = ytdlp_conf.notify_users_on_new_version
async def run(self) -> None: async def run(self) -> None:
await self._run() await self._run()
async def _run(self) -> None: async def _run(self) -> None:
if not self._version_check_enabled: release_channel = self._ytdlp_conf.release_channel
self._log.info('New "yt-dlp" version check disabled, exiting from task') if not self._ytdlp_conf.version_check_enabled:
self._log.info(
'New %s "yt-dlp" version check disabled, exiting from task',
release_channel,
)
return return
while True: while True:
self._log.info('Checking for new yt-dlp version') self._log.info('Checking for new %s yt-dlp version', release_channel)
try: try:
await self._notify_if_new_version() await self._notify_if_new_version()
except Exception: except Exception:
self._log.exception('Failed check new yt-dlp version') self._log.exception(
'Failed check new %s yt-dlp version', release_channel
)
self._log.info( self._log.info(
'Next yt-dlp version check planned at %s', 'Next %s yt-dlp version check planned at %s',
release_channel,
self._get_next_check_datetime().isoformat(' '), self._get_next_check_datetime().isoformat(' '),
) )
await asyncio.sleep(self._version_check_interval) await asyncio.sleep(self._ytdlp_conf.version_check_interval)
def _get_next_check_datetime(self) -> datetime.datetime: def _get_next_check_datetime(self) -> datetime.datetime:
return ( return (
datetime.datetime.now(datetime.timezone.utc) datetime.datetime.now(datetime.timezone.utc)
+ datetime.timedelta(seconds=self._version_check_interval) + datetime.timedelta(seconds=self._ytdlp_conf.version_check_interval)
).replace(microsecond=0) ).replace(microsecond=0)
async def _notify_if_new_version(self) -> None: async def _notify_if_new_version(self) -> None:
async for db in get_db(): async for db in get_db():
context = await self._version_checker.get_version_context(db) context = await YtdlpVersionChecker(
if context.has_new_version and self._notify_users_on_new_version: client=YtdlpGithubClient(self._ytdlp_conf.release_channel),
await self._notify_outdated(context) repository=YtdlpRepository(db),
return ).get_version_context()
if context.has_new_version:
self._log.info('yt-dlp has new version: %s', context.latest.version)
if self._ytdlp_conf.notify_users_on_new_version:
await self._notify_outdated(context)
return
if not self._startup_message_sent: if not self._startup_message_sent:
await self._notify_up_to_date( await self._notify_up_to_date(
@ -68,18 +77,19 @@ class YtdlpNewVersionNotifyTask(AbstractTask):
async def _notify_outdated(self, ctx: VersionContext) -> None: async def _notify_outdated(self, ctx: VersionContext) -> None:
text = ( text = (
f'New {code("yt-dlp")} version available: {bold(ctx.latest.version)}\n' f'New {bold(self._ytdlp_conf.release_channel)} {code("yt-dlp")} version available: '
f'{bold(ctx.latest.version)}\n'
f'Current version: {bold(ctx.current.version)}\n' f'Current version: {bold(ctx.current.version)}\n'
f'Rebuild worker with {code("docker compose build --no-cache worker")}' f'Rebuild worker with {code("docker compose build --no-cache worker && docker compose up -d -t 0 worker")}'
) )
await self._bot.send_message_admins(text) await self._bot.send_message_admins(text)
async def _notify_up_to_date( async def _notify_up_to_date(
self, ctx: VersionContext, user_ids: list[int] self, ctx: VersionContext, user_ids: list[int]
) -> None: ) -> None:
"""Send startup message that yt-dlp version is up to date.""" """Send startup message that yt-dlp version is up-to-date."""
text = ( text = (
f'{INFORMATION_EMOJI} Your {code("yt-dlp")} version ' f'{INFORMATION_EMOJI} Your {bold(self._ytdlp_conf.release_channel)} {code("yt-dlp")} '
f'{bold(ctx.current.version)} is up to date, have fun' f'version {bold(ctx.current.version)} is up to date, have fun'
) )
await self._bot.send_message_to_users(text=text, user_ids=user_ids) await self._bot.send_message_to_users(text=text, user_ids=user_ids)

View file

@ -9,7 +9,7 @@ from pyrogram.enums import ChatType
from pyrogram.types import Message from pyrogram.types import Message
from bot.core.config import settings from bot.core.config import settings
from bot.core.schema import AnonymousUserSchema, ConfigSchema, UserSchema from bot.core.schemas import AnonymousUserSchema, ConfigSchema, UserSchema
async def shallow_sleep_async(sleep_time: float = 0.1) -> None: async def shallow_sleep_async(sleep_time: float = 0.1) -> None:

View file

@ -40,3 +40,4 @@ ytdlp:
version_check_enabled: !!bool True version_check_enabled: !!bool True
version_check_interval: 86400 version_check_interval: 86400
notify_users_on_new_version: !!bool True notify_users_on_new_version: !!bool True
release_channel: "NIGHTLY"

View file

@ -55,7 +55,7 @@ class WorkerLauncher:
'Saving current yt-dlp version (%s) to the database', curr_version 'Saving current yt-dlp version (%s) to the database', curr_version
) )
async for db in get_db(): async for db in get_db():
await YtdlpRepository().create_or_update_version(curr_version, db) await YtdlpRepository(db).create_or_update_version(curr_version)
async def _create_intermediate_directories(self) -> None: async def _create_intermediate_directories(self) -> None:
"""Create temporary intermediate directories on start if they do not exist.""" """Create temporary intermediate directories on start if they do not exist."""

View file

@ -2,22 +2,31 @@ import datetime
import logging import logging
import aiohttp import aiohttp
from yt_shared.enums import YtdlpReleaseChannelType
from yt_shared.schemas.ytdlp import LatestVersion from yt_shared.schemas.ytdlp import LatestVersion
class YtdlpGithubClient: class YtdlpGithubClient:
"""yt-dlp Github version number checker.""" """yt-dlp Github version number checker."""
LATEST_TAG_URL = 'https://github.com/yt-dlp/yt-dlp/releases/latest' LATEST_TAG_URL_TPL = 'https://github.com/yt-dlp/{repository}/releases/latest'
LATEST_TAG_REPOSITORY_MAP = {
YtdlpReleaseChannelType.STABLE: 'yt-dlp',
YtdlpReleaseChannelType.NIGHTLY: 'yt-dlp-nightly-builds',
YtdlpReleaseChannelType.MASTER: 'yt-dlp-master-builds',
}
def __init__(self) -> None: def __init__(self, release_channel: YtdlpReleaseChannelType) -> None:
self._log = logging.getLogger(self.__class__.__name__) self._log = logging.getLogger(self.__class__.__name__)
self._release_channel = release_channel
async def get_latest_version(self) -> LatestVersion: async def get_latest_version(self) -> LatestVersion:
tag_url = self.LATEST_TAG_URL_TPL.format(
repository=self.LATEST_TAG_REPOSITORY_MAP[self._release_channel]
)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(self.LATEST_TAG_URL) as resp: async with session.get(tag_url) as resp:
version = resp.url.parts[-1] version = resp.url.parts[-1]
self._log.info('Latest yt-dlp version: %s', version)
return LatestVersion( return LatestVersion(
version=version, retrieved_at=datetime.datetime.utcnow() version=version, retrieved_at=datetime.datetime.utcnow()
) )

View file

@ -50,3 +50,9 @@ class DownMediaType(StrChoiceEnum):
class MediaFileType(StrChoiceEnum): class MediaFileType(StrChoiceEnum):
AUDIO = 'AUDIO' AUDIO = 'AUDIO'
VIDEO = 'VIDEO' VIDEO = 'VIDEO'
class YtdlpReleaseChannelType(StrChoiceEnum):
STABLE = 'STABLE'
NIGHTLY = 'NIGHTLY'
MASTER = 'MASTER'

View file

@ -8,17 +8,18 @@ from yt_shared.models import YTDLP
class YtdlpRepository: class YtdlpRepository:
def __init__(self) -> None: def __init__(self, db: AsyncSession) -> None:
self._log = logging.getLogger(self.__class__.__name__) self._log = logging.getLogger(self.__class__.__name__)
self._db = db
@staticmethod async def get_current_version(self) -> YTDLP:
async def get_current_version(db: AsyncSession) -> YTDLP: result = await self._db.execute(select(YTDLP))
result = await db.execute(select(YTDLP))
return result.scalar_one() return result.scalar_one()
@staticmethod async def create_or_update_version(self, current_version: str) -> None:
async def create_or_update_version(current_version: str, db: AsyncSession) -> None: row_count: int = await self._db.scalar(
row_count: int = await db.scalar(select(func.count('*')).select_from(YTDLP)) select(func.count('*')).select_from(YTDLP)
)
if row_count > 1: if row_count > 1:
raise MultipleResultsFound( raise MultipleResultsFound(
'Multiple yt-dlp version records found. Expected one.' 'Multiple yt-dlp version records found. Expected one.'
@ -30,5 +31,5 @@ class YtdlpRepository:
.values({'current_version': current_version}) .values({'current_version': current_version})
.execution_options(synchronize_session=False) .execution_options(synchronize_session=False)
) )
await db.execute(insert_or_update_stmt) await self._db.execute(insert_or_update_stmt)
await db.commit() await self._db.commit()

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import logging import logging
from sqlalchemy.ext.asyncio import AsyncSession
from yt_shared.clients.github import YtdlpGithubClient from yt_shared.clients.github import YtdlpGithubClient
from yt_shared.repositories.ytdlp import YtdlpRepository from yt_shared.repositories.ytdlp import YtdlpRepository
from yt_shared.schemas.ytdlp import CurrentVersion, LatestVersion, VersionContext from yt_shared.schemas.ytdlp import CurrentVersion, LatestVersion, VersionContext
@ -10,23 +9,23 @@ from yt_shared.schemas.ytdlp import CurrentVersion, LatestVersion, VersionContex
class YtdlpVersionChecker: class YtdlpVersionChecker:
"""yt-dlp version number checker.""" """yt-dlp version number checker."""
LATEST_TAG_URL = 'https://github.com/yt-dlp/yt-dlp/releases/latest' def __init__(self, client: YtdlpGithubClient, repository: YtdlpRepository) -> None:
def __init__(self) -> None:
self._log = logging.getLogger(self.__class__.__name__) self._log = logging.getLogger(self.__class__.__name__)
self._ytdlp_repository = YtdlpRepository() self._ytdlp_repository = repository
self._ytdlp_client = YtdlpGithubClient() self._ytdlp_client = client
async def get_version_context(self, db: AsyncSession) -> VersionContext: async def get_version_context(self) -> VersionContext:
latest, current = await asyncio.gather( latest, current = await asyncio.gather(
self.get_latest_version(), self.get_current_version(db) self.get_latest_version(), self.get_current_version()
) )
return VersionContext(latest=latest, current=current) return VersionContext(latest=latest, current=current)
async def get_latest_version(self) -> LatestVersion: async def get_latest_version(self) -> LatestVersion:
return await self._ytdlp_client.get_latest_version() latest_version = await self._ytdlp_client.get_latest_version()
self._log.info('Latest yt-dlp version: %s', latest_version.version)
return latest_version
async def get_current_version(self, db: AsyncSession) -> CurrentVersion: async def get_current_version(self) -> CurrentVersion:
ytdlp_ = await self._ytdlp_repository.get_current_version(db) ytdlp_ = await self._ytdlp_repository.get_current_version()
self._log.info('Current yt-dlp version: %s', ytdlp_.current_version) self._log.info('Current yt-dlp version: %s', ytdlp_.current_version)
return CurrentVersion.model_validate(ytdlp_) return CurrentVersion.model_validate(ytdlp_)