diff --git a/.gitignore b/.gitignore index b58e201..2f4640f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/lib/jamendo.py b/lib/jamendo.py new file mode 100644 index 0000000..6ae8387 --- /dev/null +++ b/lib/jamendo.py @@ -0,0 +1,55 @@ +from enum import Enum +from typing import Literal +import requests +import urllib.parse + +API_BASE = 'https://api.jamendo.com/v3.0/' +ImageSize = Literal[25, 35, 50, 55, 60, 65, 70, 75, 85, 100, 130, 150, 200, 300, 400, 500, 600] +AudioFormat = Literal['mp31', 'mp32', 'ogg', 'flac'] +VocalInstrumental = Literal['vocal', 'instrumental'] +AcousticElectric = Literal['acoustic', 'electric'] +Order = Literal['relevance', 'buzzrate', 'downloads_week', 'downloads_month', 'downloads_total', 'listens_week', 'listens_month', 'listens_total', 'popularity_week', 'popularity_month', 'popularity_total', 'name', 'album_name', 'artist_name', 'releasedate', 'duration', 'id'] +Speed = Literal['verylow', 'low', 'medium', 'high', 'veryhigh'] + +class JamendoClient: + def __init__(self, client_id: str): + self._client_id = client_id + self._session = requests.session() + + def tracks(self, tags: str, offset: int = 0, order: Order = 'relevance', + audioformat: AudioFormat = 'ogg', imagesize: ImageSize = 600, + vocalinstrumental: VocalInstrumental = None, + acousticelectric: AcousticElectric = None, + speed: Speed = None, ccsa: bool = False, + ccnd: bool = False, ccnc: bool = False, + ): + params = { + 'client_id': self._client_id, + 'offset': offset, + 'format': 'json', + 'tags': urllib.parse.quote(tags), + 'audioformat': audioformat, + 'imagesize': imagesize, + 'ccsa': ccsa, + 'ccnd': ccnd, + 'ccnc': ccnc, + 'limit': 5 + } + if vocalinstrumental: params['vocalinstrumental'] = urllib.parse.quote(vocalinstrumental) + if acousticelectric: params['acousticelectric'] = urllib.parse.quote(acousticelectric) + if speed: params['speed'] = urllib.parse.quote(speed) + + url = urllib.parse.urljoin(API_BASE, f'tracks/?{urllib.parse.urlencode(params)}') + + with self._session.get(url) as r: + r.raise_for_status() + result = r.json() + + if 'results' in result: + return result['results'] + + def download(self, url: str, fp): + with self._session.get(url, stream=True) as r: + r.raise_for_status() + for chunk in r.iter_content(chunk_size=1024): + fp.write(chunk) \ No newline at end of file diff --git a/lib/player.py b/lib/player.py new file mode 100644 index 0000000..ef93c16 --- /dev/null +++ b/lib/player.py @@ -0,0 +1,65 @@ +import threading +from queue import Queue, Empty +import miniaudio +import logging +from lib.renderer import Renderer + +class Player: + def __init__(self, renderer: Renderer, play_finished_handler): + self._queue = Queue() + self._next_event = threading.Event() + self._exit_event = threading.Event() + self._logger = logging.getLogger('Jamendo Player') + self._renderer = renderer + self._play_finished_handler = play_finished_handler + threading.Thread(target=self.worker, args=()).start() + + def worker(self): + while True: + try: + item = self._queue.get(block=True, timeout=1) + if item: + self._renderer.render(item['cover_file'], item['artist'], item['album'], item['title']) + self._logger.info('playing {artist}, {album}, {title}...'.format(artist=item['artist'], album=item['album'], title=item['title'])) + file_stream = miniaudio.stream_file(item['audio_file']) + callback_stream = miniaudio.stream_with_callbacks(file_stream, end_callback=self.stream_end_callback) + next(callback_stream) + with miniaudio.PlaybackDevice() as device: + device.start(callback_stream) + self._next_event.wait() + self._next_event.clear() + self._queue.task_done() + self._play_finished_handler(item['audio_file'], item['cover_file']) + except Empty: + pass + + if self._exit_event.is_set(): + break + + def stream_end_callback(self): + self.next() + + def add_file(self, audio_file, artist: str, album: str, title: str, cover_file): + self._queue.put( + { + 'audio_file': audio_file, + 'artist': artist, + 'album': album, + 'title': title, + 'cover_file': cover_file + } + ) + + @property + def queue_size(self): + return self._queue.qsize() + + def next(self): + self._next_event.set() + + def quit(self): + self._exit_event.set() + self.next() + while not self._queue.empty(): + item = self._queue.get() + self._play_finished_handler(item['audio_file'], item['cover_file']) \ No newline at end of file diff --git a/lib/png_renderer.py b/lib/png_renderer.py new file mode 100644 index 0000000..d8d1546 --- /dev/null +++ b/lib/png_renderer.py @@ -0,0 +1,44 @@ + +from lib.renderer import Renderer +from PIL import Image, ImageDraw, ImageFont, ImageFile + +class PNG_Renderer(Renderer): + + def __init__(self, config): + self._config = config + ImageFile.LOAD_TRUNCATED_IMAGES = True + + def render(self, cover, artist, album, title): + title_font = ImageFont.truetype(font=self._config['TitleFont'], size=int(self._config['TitleFontSize'])) + title_y = 0 + title_height = title_font.getbbox('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')[3] + text_width = title_font.getbbox(title)[2] + + artist_font = ImageFont.truetype(font=self._config['ArtistFont'], size=int(self._config['ArtistFontSize'])) + artist_y = title_y + title_height + 5 + artist_height = artist_font.getbbox('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')[3] + if artist_font.getbbox(artist)[2] > text_width: text_width = artist_font.getbbox(artist)[2] + + album_font = ImageFont.truetype(font=self._config['AlbumFont'], size=int(self._config['AlbumFontSize'])) + album_y = artist_y + artist_height + 2 + album_height = album_font.getbbox('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')[3] + if album_font.getbbox(album)[2] > text_width: text_width = album_font.getbbox(album)[2] + + image_height = album_y + album_height + 10 + image_width = text_width + image_height + 40 + image = Image.new('RGBA', (image_width, image_height), color='#' + self._config['BackgroundColor']) + + draw = ImageDraw.Draw(image) + text_x = 0 + + if self._config['Cover'] == '1': + cover = Image.open(cover) + cover.thumbnail((image_height, image_height), Image.ANTIALIAS) + image.paste(cover) + text_x = image_height + 20 + + draw.text((text_x,title_y), title, font=title_font, fill='#' + self._config['TitleFontColor']) + draw.text((text_x,artist_y), artist, font=artist_font, fill='#' + self._config['TitleFontColor']) + draw.text((text_x,album_y), album, font=album_font, fill='#' + self._config['TitleFontColor']) + + image.save(self._config['OutputPath']) \ No newline at end of file diff --git a/lib/renderer.py b/lib/renderer.py new file mode 100644 index 0000000..62c7eab --- /dev/null +++ b/lib/renderer.py @@ -0,0 +1,4 @@ +class Renderer: + + def render(self, cover: str, artist: str, album: str, title: str): + pass \ No newline at end of file