diff --git a/.gitignore b/.gitignore index e7db7e4..9ef8c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ wheels/ .idea .env *.session +*.session-journal oauth.json \ No newline at end of file diff --git a/alembic/versions/04289c560c5c_edit_track_spotify_refreshed_at_to_.py b/alembic/versions/04289c560c5c_edit_track_spotify_refreshed_at_to_.py deleted file mode 100644 index 29f3668..0000000 --- a/alembic/versions/04289c560c5c_edit_track_spotify_refreshed_at_to_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""edit Track.spotify_refreshed_at to spotify_refresh_at - -Revision ID: 04289c560c5c -Revises: 45a6cad48d51 -Create Date: 2025-04-04 15:52:24.278867 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '04289c560c5c' -down_revision: Union[str, None] = '45a6cad48d51' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('users', sa.Column('spotify_refresh_at', sa.Integer(), nullable=False)) - op.drop_column('users', 'spotify_refreshed_at') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('users', sa.Column('spotify_refreshed_at', sa.INTEGER(), autoincrement=False, nullable=False)) - op.drop_column('users', 'spotify_refresh_at') - # ### end Alembic commands ### diff --git a/alembic/versions/45a6cad48d51_add_md5_checksum.py b/alembic/versions/45a6cad48d51_add_md5_checksum.py deleted file mode 100644 index 78707b5..0000000 --- a/alembic/versions/45a6cad48d51_add_md5_checksum.py +++ /dev/null @@ -1,32 +0,0 @@ -"""add md5 checksum - -Revision ID: 45a6cad48d51 -Revises: 6c98fbc4386e -Create Date: 2025-04-04 15:02:09.437623 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '45a6cad48d51' -down_revision: Union[str, None] = '6c98fbc4386e' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('tracks', sa.Column('telegram_md5_checksum', sa.String(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('tracks', 'telegram_md5_checksum') - # ### end Alembic commands ### diff --git a/alembic/versions/6c98fbc4386e_init_migrations.py b/alembic/versions/d99a58c0c384_edit_track.py similarity index 57% rename from alembic/versions/6c98fbc4386e_init_migrations.py rename to alembic/versions/d99a58c0c384_edit_track.py index 142aa50..0b69ab2 100644 --- a/alembic/versions/6c98fbc4386e_init_migrations.py +++ b/alembic/versions/d99a58c0c384_edit_track.py @@ -1,8 +1,8 @@ -"""init migrations +"""edit Track -Revision ID: 6c98fbc4386e -Revises: f7473c0d02c7 -Create Date: 2025-04-02 23:38:49.273908 +Revision ID: d99a58c0c384 +Revises: +Create Date: 2025-04-04 22:08:49.345416 """ from typing import Sequence, Union @@ -12,8 +12,8 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '6c98fbc4386e' -down_revision: Union[str, None] = 'f7473c0d02c7' +revision: str = 'd99a58c0c384' +down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -23,19 +23,29 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('tracks', sa.Column('id', sa.Integer(), nullable=False), + sa.Column('telegram_id', sa.BigInteger(), nullable=True), + sa.Column('telegram_access_hash', sa.BigInteger(), nullable=True), + sa.Column('telegram_file_reference', sa.LargeBinary(), nullable=True), sa.Column('spotify_id', sa.String(), nullable=False), - sa.Column('telegram_id', sa.Integer(), nullable=True), sa.Column('name', sa.String(), nullable=False), sa.Column('artist', sa.String(), nullable=False), sa.Column('cover_url', sa.String(), nullable=False), sa.Column('used_times', sa.String(), nullable=False), sa.PrimaryKeyConstraint('id') ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('spotify_access_token', sa.String(), nullable=False), + sa.Column('spotify_refresh_token', sa.String(), nullable=False), + sa.Column('spotify_refresh_at', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') op.drop_table('tracks') # ### end Alembic commands ### diff --git a/alembic/versions/f7473c0d02c7_init_migrations.py b/alembic/versions/f7473c0d02c7_init_migrations.py deleted file mode 100644 index a75bbc5..0000000 --- a/alembic/versions/f7473c0d02c7_init_migrations.py +++ /dev/null @@ -1,38 +0,0 @@ -"""init migrations - -Revision ID: f7473c0d02c7 -Revises: -Create Date: 2025-04-02 23:37:19.633954 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'f7473c0d02c7' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('spotify_access_token', sa.String(), nullable=False), - sa.Column('spotify_refresh_token', sa.String(), nullable=False), - sa.Column('spotify_refreshed_at', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('users') - # ### end Alembic commands ### diff --git a/app/MusicProvider/SpotifyStrategy.py b/app/MusicProvider/SpotifyStrategy.py index 28b2151..2ccf947 100644 --- a/app/MusicProvider/SpotifyStrategy.py +++ b/app/MusicProvider/SpotifyStrategy.py @@ -52,7 +52,7 @@ class SpotifyStrategy(MusicProviderStrategy): 'Content-Type': 'application/json' } user_params = { - 'limit': 5 + 'limit': 1 } res = [] async with aiohttp.ClientSession() as session: diff --git a/app/__init__.py b/app/__init__.py index c394eb2..9b9ae38 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,18 +8,19 @@ from telethon.tl.types import ( TypeInputFile, InputFile, DocumentAttributeAudio, - InputPeerSelf + InputPeerSelf, InputDocument ) from telethon import functions from telethon.utils import get_input_document import urllib.parse from mutagen.id3 import ID3, APIC import logging -import uuid +from cachetools import LRUCache + from app.MusicProvider import MusicProviderContext, SpotifyStrategy from app.config import config -from app.dependencies import get_session +from app.dependencies import get_session, get_session_context from app.models import Track from app.youtube_api import name_to_youtube, download_youtube @@ -33,6 +34,7 @@ logger = logging.getLogger(__name__) client = TelegramClient('nowplaying', config.api_id, config.api_hash) client.parse_mode = 'html' dummy_file: TypeInputFile = None +cache = LRUCache(maxsize=100) def get_link_account_keyboard(): @@ -94,20 +96,22 @@ async def update_dummy_file_cover(cover_url: str): async def build_response(e: events.InlineQuery.Event, track: Track): - track_id = f'{track.spotify_id}__{track.name}__{track.artist}' if not track.telegram_id: await update_dummy_file() buttons = [Button.inline('Loading', 'loading')] else: global dummy_file - dummy_file = InputFile(int(track.telegram_id), 1, track.name, track.telegram_md5_checksum) + dummy_file = InputDocument( + id=track.telegram_id, + access_hash=track.telegram_access_hash, + file_reference=track.telegram_file_reference + ) buttons = None - track_id += '__cached' return e.builder.document( file=dummy_file, title=track.name, description=track.artist, - id=track_id, + id=track.spotify_id, mime_type='audio/mpeg', attributes=[ DocumentAttributeAudio( @@ -131,17 +135,14 @@ async def query_list(e: events.InlineQuery.Event): for track in tracks: track = await context.get_cached_track(track) + cache[track.spotify_id] = track result.append(await build_response(e, track)) await e.answer(result) -@client.on(events.Raw([UpdateBotInlineSend])) -async def send_track(e: UpdateBotInlineSend): - if e.id.endswith('__cached'): - return - track_id, name, artist = e.id.split('__')[:4] +async def get_track_file(track): yt_id = await asyncio.get_event_loop().run_in_executor( - None, functools.partial(name_to_youtube, f'{name} - {artist}') + None, functools.partial(name_to_youtube, f'{track.name} - {track.artist}') ) audio, duration = await download_youtube(yt_id) _, media, _ = await client._file_to_media( @@ -150,8 +151,8 @@ async def send_track(e: UpdateBotInlineSend): DocumentAttributeAudio( duration=duration, voice=False, - title=name, - performer=artist, + title=track.name, + performer=track.artist, waveform=None, )] ) @@ -160,9 +161,28 @@ async def send_track(e: UpdateBotInlineSend): InputPeerSelf(), media=media ) ) - file = get_input_document(uploaded_media.document) - await client.edit_message(e.msg_id, file=file, text=get_track_links(track_id)) + return get_input_document(uploaded_media.document) + + +async def cache_file(track): + async with get_session_context() as session: + session.add(track) + await session.commit() + + +@client.on(events.Raw([UpdateBotInlineSend])) +async def send_track(e: UpdateBotInlineSend): + track = cache[e.id] + if track.telegram_id: + return + + file = await get_track_file(track) + track.telegram_id = file.id + track.telegram_access_hash = file.access_hash + track.telegram_file_reference = file.file_reference + await cache_file(track) + await client.edit_message(e.msg_id, file=file, text=get_track_links(e.id)) async def main(): diff --git a/app/models/track.py b/app/models/track.py index e560916..b3d040f 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -2,18 +2,21 @@ from typing import Optional from app.models import Base from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import BigInteger, LargeBinary, Integer + class Track(Base): __tablename__ = 'tracks' id: Mapped[int] = mapped_column(primary_key=True) - telegram_id: Mapped[Optional[int]] - telegram_md5_checksum: Mapped[Optional[str]] + telegram_id: Mapped[Optional[int]] = mapped_column(BigInteger) + telegram_access_hash: Mapped[Optional[int]] = mapped_column(BigInteger) + telegram_file_reference: Mapped[Optional[bytes]] = mapped_column(LargeBinary) spotify_id: Mapped[str] name: Mapped[str] artist: Mapped[str] cover_url: Mapped[str] - used_times: Mapped[str] + used_times: Mapped[str] = mapped_column(Integer, default=1) diff --git a/pyproject.toml b/pyproject.toml index 9f2e526..fc9fc4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "aiohttp>=3.11.14", "alembic>=1.15.2", "asyncpg>=0.30.0", + "cachetools>=5.5.2", "fastapi>=0.115.12", "mediafile>=0.13.0", "mutagen>=1.47.0", diff --git a/uv.lock b/uv.lock index 9726b25..7edf106 100644 --- a/uv.lock +++ b/uv.lock @@ -116,6 +116,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -357,6 +366,7 @@ dependencies = [ { name = "aiohttp" }, { name = "alembic" }, { name = "asyncpg" }, + { name = "cachetools" }, { name = "fastapi" }, { name = "mediafile" }, { name = "mutagen" }, @@ -376,6 +386,7 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.11.14" }, { name = "alembic", specifier = ">=1.15.2" }, { name = "asyncpg", specifier = ">=0.30.0" }, + { name = "cachetools", specifier = ">=5.5.2" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "mediafile", specifier = ">=0.13.0" }, { name = "mutagen", specifier = ">=1.47.0" },