diff --git a/app/MusicProvider/SpotifyStrategy.py b/app/MusicProvider/SpotifyStrategy.py index 2ccf947..573c819 100644 --- a/app/MusicProvider/SpotifyStrategy.py +++ b/app/MusicProvider/SpotifyStrategy.py @@ -1,9 +1,10 @@ +import asyncio import base64 import time import aiohttp -from app.config import config +from app.config import config, spotify_creds from app.MusicProvider.Strategy import MusicProviderStrategy from app.dependencies import get_session, get_session_context from sqlalchemy import select, update @@ -11,10 +12,38 @@ from sqlalchemy import select, update from app.models import User, Track -creds = base64.b64encode(config.spotify.client_id.encode() + b':' + config.spotify.client_secret.encode()).decode("utf-8") +def convert_track(track: dict): + if track['type'] != 'track': + return None + + return Track( + name=track['name'], + artist=', '.join(x['name'] for x in track['artists']), + cover_url=track['album']['images'][0]['url'], + spotify_id=track['id'] + ) + + +async def refresh_token(refresh_token): + token_headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Basic ' + spotify_creds + } + token_data = { + "grant_type": "refresh_token", + "refresh_token": refresh_token + } + async with aiohttp.ClientSession() as session: + resp = await session.post("https://accounts.spotify.com/api/token", data=token_data, headers=token_headers) + resp = await resp.json() + return resp['access_token'], resp['expires_in'] class SpotifyStrategy(MusicProviderStrategy): + def __init__(self, user_id): + super().__init__(user_id) + self.token = None + async def handle_token(self): async with get_session_context() as session: res = await session.execute(select(User).where(User.id == self.user_id)) @@ -25,55 +54,44 @@ class SpotifyStrategy(MusicProviderStrategy): if int(time.time()) < user.spotify_refresh_at: return user.spotify_access_token - token_headers = { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': 'Basic ' + creds - } - token_data = { - "grant_type": "refresh_token", - "refresh_token": user.spotify_refresh_token - } - async with aiohttp.ClientSession() as session: - resp = await session.post("https://accounts.spotify.com/api/token", data=token_data, headers=token_headers) - resp = await resp.json() - token, expires_in = resp['access_token'], resp['expires_in'] + token, expires_in = await refresh_token(user.spotify_refresh_token) async with get_session_context() as session: await session.execute( update(User).where(User.id == self.user_id).values(spotify_access_token=token, - spotify_refresh_at=int(time.time()) + int(expires_in) - ) + spotify_refresh_at=int(time.time()) + int(expires_in)) ) await session.commit() return token - async def get_tracks(self, token) -> list[Track]: + async def request(self, endpoint, token): user_headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } - user_params = { - 'limit': 1 - } - res = [] async with aiohttp.ClientSession() as session: - resp = await session.get('https://api.spotify.com/v1/me/player/recently-played', params=user_params, headers=user_headers) - data = await resp.json() - for i, el in enumerate(data['items']): - track = el['track'] - resp = await session.get(f'https://api.spotify.com/v1/albums/{track['album']['id']}', headers=user_headers) - album = await resp.json() - cover = album['images'][0] - res.append(Track( - name=track['name'], - artist=', '.join(x['name'] for x in track['artists']), - cover_url=cover['url'], - spotify_id=track['id'] - )) - return res + resp = await session.get(f'https://api.spotify.com/v1{endpoint}', headers=user_headers) + return await resp.json() + + async def get_tracks(self, token) -> list[Track]: + current, recent = await asyncio.gather( + self.request('/me/player/currently-playing', token), + self.request('/me/player/recently-played', token) + ) + tracks = [] + if current: + tracks.append(convert_track(current['item'])) + for item in recent['items']: + tracks.append(convert_track(item['track'])) + + tracks = [x for x in tracks if x] + return tracks async def fetch_track(self, track: Track): async with get_session_context() as session: resp = await session.execute( select(Track).where(Track.spotify_id == track.spotify_id) ) - return resp.scalars().first() \ No newline at end of file + return resp.scalars().first() + + +__all__ = ['SpotifyStrategy'] diff --git a/app/__init__.py b/app/__init__.py index 9b9ae38..991db1e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,12 +1,13 @@ import asyncio import functools import io +import time + import aiohttp from telethon import TelegramClient, events, Button from telethon.tl.types import ( UpdateBotInlineSend, TypeInputFile, - InputFile, DocumentAttributeAudio, InputPeerSelf, InputDocument ) @@ -16,6 +17,7 @@ import urllib.parse from mutagen.id3 import ID3, APIC import logging from cachetools import LRUCache +import jwt from app.MusicProvider import MusicProviderContext, SpotifyStrategy @@ -49,12 +51,17 @@ async def start(e: events.NewMessage.Event): @client.on(events.CallbackQuery(pattern='connect_spotify')) async def connect_spotify(e: events.CallbackQuery.Event): + payload = { + 'tg_id': e.sender_id, + 'exp': int(time.time()) + 300 + } + params = { 'client_id': config.spotify.client_id, 'response_type': 'code', 'redirect_uri': config.spotify.redirect, - 'scope': 'user-read-recently-played', - 'state': f'tg_{e.sender_id}' + 'scope': 'user-read-recently-played user-read-currently-playing', + 'state': jwt.encode(payload, config.jwt_secret, algorithm='HS256') } await e.respond('Link your Spotify account', buttons=[Button.url('Link', f"https://accounts.spotify.com/authorize?{urllib.parse.urlencode(params)}")]) @@ -130,7 +137,7 @@ async def build_response(e: events.InlineQuery.Event, track: Track): @client.on(events.InlineQuery()) async def query_list(e: events.InlineQuery.Event): context = MusicProviderContext(SpotifyStrategy(e.sender_id)) - tracks = await context.get_tracks() + tracks = (await context.get_tracks())[:5] result = [] for track in tracks: diff --git a/app/callback_listener.py b/app/callback_listener.py index d5874d2..712bf90 100644 --- a/app/callback_listener.py +++ b/app/callback_listener.py @@ -1,36 +1,44 @@ from contextlib import asynccontextmanager -from fastapi import FastAPI, Depends +from fastapi import FastAPI, Depends, Request from sqlalchemy.ext.asyncio import AsyncSession -from fastapi.responses import HTMLResponse +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles import uvicorn from telethon import TelegramClient -import base64 import aiohttp import time +import jwt from app.dependencies import get_session from app.models.user import User -from config import config +from config import config, spotify_creds client = TelegramClient('nowplaying_callback', config.api_id, config.api_hash) @asynccontextmanager async def lifespan(app: FastAPI): - global r await client.connect() await client.sign_in(bot_token=config.bot_token) yield app = FastAPI(lifespan=lifespan) -creds = base64.b64encode(config.spotify.client_id.encode() + b':' + config.spotify.client_secret.encode()).decode( - "utf-8") +app.mount('/static', StaticFiles(directory='static', html=True), name='static') -async def get_spotify_token(code: str, user_id: int): +class LinkException(Exception): + pass + + +@app.exception_handler(LinkException) +async def unicorn_exception_handler(request: Request, exc: LinkException): + return FileResponse('static/error.html', status_code=400) + + +async def get_spotify_token(code: str): token_headers = { - "Authorization": "Basic " + creds, + "Authorization": "Basic " + spotify_creds, "Content-Type": "application/x-www-form-urlencoded" } token_data = { @@ -43,57 +51,20 @@ async def get_spotify_token(code: str, user_id: int): resp = await session.post("https://accounts.spotify.com/api/token", data=token_data, headers=token_headers) resp = await resp.json() + if 'access_token' not in resp: + raise LinkException() return resp['access_token'], resp['refresh_token'], int(resp['expires_in']) -def generate_success_reponse(): - content = """ - -
- - -Try to link account again
+ + \ No newline at end of file diff --git a/static/success.html b/static/success.html new file mode 100644 index 0000000..f053d8b --- /dev/null +++ b/static/success.html @@ -0,0 +1,47 @@ + + + + + +Now you can return to the bot.
+ + \ No newline at end of file diff --git a/success.html b/success.html deleted file mode 100644 index 8d71719..0000000 --- a/success.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - -