Add cover art cache, more caching and optimization for metadata, global metadata search, fix some scaling issues, add example tracks for albums and artists, urls for artists, remove play button

This commit is contained in:
csd4ni3l
2025-07-09 21:09:46 +02:00
parent a0a0cf1d75
commit 951ae41481
10 changed files with 498 additions and 238 deletions

3
.gitignore vendored
View File

@@ -181,4 +181,5 @@ logs/
logs logs
settings.json settings.json
bin/ bin/
metadata_cache.json metadata_cache.json
cover_cache/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/graphics/person.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

117
menus/global_search.py Normal file
View File

@@ -0,0 +1,117 @@
import arcade, arcade.gui
from utils.preload import music_icon, person_icon, button_texture, button_hovered_texture
from utils.constants import button_style
from utils.utils import Card, MouseAwareScrollArea, get_wordwrapped_text
from utils.online_metadata import search_recordings, search_artists, search_albums, get_artists_metadata, get_album_metadata
from arcade.gui.experimental.focus import UIFocusGroup
from arcade.gui.experimental.scroll_area import UIScrollBar
class GlobalSearch(arcade.gui.UIView):
def __init__(self, pypresence_client, *args):
super().__init__()
self.args = args
self.pypresence_client = pypresence_client
self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1)))
def on_show_view(self):
super().on_show_view()
self.anchor.detect_focusable_widgets()
self.ui_box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.99, 0.99), space_between=10), anchor_x="center", anchor_y="center")
self.search_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(1, 0.075), space_between=10, vertical=False))
self.back_button = self.search_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50))
self.back_button.on_click = lambda event: self.main_exit()
self.search_bar = self.search_box.add(arcade.gui.UIInputText(width=self.window.width / 2, height=self.window.height / 20, font_size=20))
self.search_bar.on_change = lambda event: self.fix_searchbar_text()
self.search_type_dropdown = self.search_box.add(arcade.gui.UIDropdown(options=["Music", "Artist", "Album"], default="Music", primary_style=button_style, active_style=button_style, dropdown_style=button_style, width=self.window.width / 4, height=self.window.height / 20))
self.scroll_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(1, 0.925), space_between=15, vertical=False))
self.scroll_area = MouseAwareScrollArea(size_hint=(1, 1))
self.scroll_area.scroll_speed = -50
self.scroll_box.add(self.scroll_area)
self.scrollbar = UIScrollBar(self.scroll_area)
self.scrollbar.size_hint = (0.02, 1)
self.scroll_box.add(self.scrollbar)
self.search_results_grid = arcade.gui.UIGridLayout(horizontal_spacing=25, vertical_spacing=25, column_count=8, row_count=999)
self.scroll_area.add(self.search_results_grid)
self.nothing_searched_label = self.anchor.add(arcade.gui.UILabel(text="Search for something to get results!", font_name="Roboto", font_size=24), anchor_x="center", anchor_y="center")
self.nothing_searched_label.visible = True
def fix_searchbar_text(self):
self.search_bar.text = self.search_bar.text.encode("ascii", "ignore").decode().strip("\n")
def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ENTER:
self.fix_searchbar_text()
self.search()
def search(self):
search_type = self.search_type_dropdown.value
search_term = self.search_bar.text
self.search_results_grid.clear()
if search_type == "Music":
recordings = search_recordings(search_term)
self.nothing_searched_label.visible = not bool(recordings)
for n, metadata in enumerate(recordings):
row = n // 7
col = n % 7
card = self.search_results_grid.add(Card(music_icon, get_wordwrapped_text(metadata[1]), get_wordwrapped_text(metadata[0]), width=self.window.width / 7, height=self.window.width / 7), row=row, column=col)
card.button.on_click = lambda event, metadata=metadata: self.open_metadata_viewer(metadata[2], metadata[0], metadata[1])
elif search_type == "Artist":
artists = search_artists(search_term)
self.nothing_searched_label.visible = not bool(artists)
for n, metadata in enumerate(artists):
row = n // 7
col = n % 7
card = self.search_results_grid.add(Card(person_icon, get_wordwrapped_text(metadata[0]), None, width=self.window.width / 7, height=self.window.width / 4.5), row=row, column=col)
card.button.on_click = lambda event, metadata=metadata: self.open_metadata_viewer(metadata[1])
else:
albums = search_albums(search_term)
self.nothing_searched_label.visible = not bool(albums)
for n, metadata in enumerate(albums):
row = n // 7
col = n % 7
card = self.search_results_grid.add(Card(music_icon, get_wordwrapped_text(metadata[1]), get_wordwrapped_text(metadata[0]), width=self.window.width / 7, height=self.window.width / 7), row=row, column=col)
card.button.on_click = lambda event, metadata=metadata: self.open_metadata_viewer(metadata[2])
self.search_results_grid.row_count = row + 1
self.search_results_grid._update_size_hints()
def open_metadata_viewer(self, musicbrainz_id, artist=None, title=None):
content_type = self.search_type_dropdown.value.lower()
from menus.metadata_viewer import MetadataViewer
if content_type == "music":
self.window.show_view(MetadataViewer(self.pypresence_client, content_type, {"artist": artist, "title": title, "id": musicbrainz_id}, None, *self.args))
elif content_type == "artist":
self.window.show_view(MetadataViewer(self.pypresence_client, content_type, get_artists_metadata([musicbrainz_id])))
elif content_type == "album":
self.window.show_view(MetadataViewer(self.pypresence_client, content_type, {musicbrainz_id: get_album_metadata(musicbrainz_id)}))
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client, *self.args))

View File

@@ -3,7 +3,7 @@ import arcade, pyglet
from utils.preload import * from utils.preload import *
from utils.constants import button_style, slider_style, audio_extensions, discord_presence_id from utils.constants import button_style, slider_style, audio_extensions, discord_presence_id
from utils.utils import FakePyPresence, UIFocusTextureButton, MusicItem, MouseAwareScrollArea from utils.utils import FakePyPresence, UIFocusTextureButton, Card, MouseAwareScrollArea, get_wordwrapped_text
from utils.music_handling import update_last_play_statistics, extract_metadata_and_thumbnail, adjust_volume, truncate_end from utils.music_handling import update_last_play_statistics, extract_metadata_and_thumbnail, adjust_volume, truncate_end
from utils.file_watching import watch_directories, watch_files from utils.file_watching import watch_directories, watch_files
@@ -123,7 +123,7 @@ class Main(arcade.gui.UIView):
self.scrollbar.size_hint = (0.02, 1) self.scrollbar.size_hint = (0.02, 1)
self.scroll_box.add(self.scrollbar) self.scroll_box.add(self.scrollbar)
self.music_grid = arcade.gui.UIGridLayout(horizontal_spacing=10, vertical_spacing=10, row_count=99, column_count=7) self.music_grid = arcade.gui.UIGridLayout(horizontal_spacing=10, vertical_spacing=10, row_count=99, column_count=8)
self.scroll_area.add(self.music_grid) self.scroll_area.add(self.music_grid)
# Controls # Controls
@@ -179,7 +179,10 @@ class Main(arcade.gui.UIView):
self.volume_slider.on_change = self.on_volume_slider_change self.volume_slider.on_change = self.on_volume_slider_change
self.tools_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=15, vertical=False), anchor_x="right", anchor_y="bottom", align_x=-15, align_y=15) self.tools_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=15, vertical=False), anchor_x="right", anchor_y="bottom", align_x=-15, align_y=15)
self.global_search_button = self.tools_box.add(UIFocusTextureButton(texture=global_search_icon, style=button_style))
self.global_search_button.on_click = lambda event: self.global_search()
self.metadata_button = self.tools_box.add(UIFocusTextureButton(texture=metadata_icon, style=button_style)) self.metadata_button = self.tools_box.add(UIFocusTextureButton(texture=metadata_icon, style=button_style))
self.metadata_button.on_click = lambda event: self.view_metadata(self.current_music_path) if self.current_music_path else None self.metadata_button.on_click = lambda event: self.view_metadata(self.current_music_path) if self.current_music_path else None
@@ -291,7 +294,7 @@ class Main(arcade.gui.UIView):
def view_metadata(self, file_path): def view_metadata(self, file_path):
from menus.metadata_viewer import MetadataViewer from menus.metadata_viewer import MetadataViewer
self.window.show_view(MetadataViewer(self.pypresence_client, "music", self.file_metadata[file_path], file_path, self.current_tab, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) self.window.show_view(MetadataViewer(self.pypresence_client, "file", self.file_metadata[file_path], file_path, self.current_tab, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle))
def show_content(self, tab, content_type): def show_content(self, tab, content_type):
for music_button in self.music_buttons.values(): for music_button in self.music_buttons.values():
@@ -309,8 +312,12 @@ class Main(arcade.gui.UIView):
if not self.search_term == "": if not self.search_term == "":
matches = process.extract(self.search_term, original_content, limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio) matches = process.extract(self.search_term, original_content, limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio)
self.highest_score_file = f"{self.current_tab}/{matches[0][0]}" if matches:
content_to_show = [match[0] for match in matches] self.highest_score_file = f"{self.current_tab}/{matches[0][0]}"
content_to_show = [match[0] for match in matches]
else:
self.highest_score_file = ""
self.content_to_show = []
else: else:
self.highest_score_file = "" self.highest_score_file = ""
@@ -321,8 +328,8 @@ class Main(arcade.gui.UIView):
row, col = 0, 0 row, col = 0, 0
for n, music_filename in enumerate(content_to_show): for n, music_filename in enumerate(content_to_show):
row = n // 7 row = n // 8
col = n % 7 col = n % 8
if self.current_mode == "files": if self.current_mode == "files":
music_path = f"{tab}/{music_filename}" music_path = f"{tab}/{music_filename}"
@@ -331,17 +338,17 @@ class Main(arcade.gui.UIView):
metadata = self.file_metadata[music_path] metadata = self.file_metadata[music_path]
self.music_buttons[music_path] = self.music_grid.add(MusicItem(metadata=metadata, width=self.window.width / 8, height=self.window.height / 5), row=row, column=col) self.music_buttons[music_path] = self.music_grid.add(Card(metadata["thumbnail"], get_wordwrapped_text(metadata["title"]), get_wordwrapped_text(metadata["artist"]), width=self.window.width / 9, height=self.window.width / 9), row=row, column=col)
self.music_buttons[music_path].button.on_click = lambda event, music_path=music_path: self.music_button_click(event, music_path) self.music_buttons[music_path].button.on_click = lambda event, music_path=music_path: self.music_button_click(event, music_path)
row = (n + 1) // 7 row = (n + 1) // 8
col = (n + 1) % 7 col = (n + 1) % 8
self.music_grid.row_count = row + 1 self.music_grid.row_count = row + 1
self.music_grid._update_size_hints() self.music_grid._update_size_hints()
if self.current_mode == "playlist": if self.current_mode == "playlist":
self.music_buttons["add_music"] = self.music_grid.add(MusicItem(metadata=None, width=self.window.width / 8, height=self.window.height / 5), row=row, column=col) self.music_buttons["add_music"] = self.music_grid.add(Card(music_icon, "Add Music", None, width=self.window.width / 9, height=self.window.width / 9), row=row, column=col)
self.music_buttons["add_music"].button.on_click = lambda event: self.add_music() self.music_buttons["add_music"].button.on_click = lambda event: self.add_music()
self.anchor.detect_focusable_widgets() self.anchor.detect_focusable_widgets()
@@ -358,8 +365,7 @@ class Main(arcade.gui.UIView):
with open("settings.json", "w") as file: with open("settings.json", "w") as file:
file.write(json.dumps(self.settings_dict, indent=4)) file.write(json.dumps(self.settings_dict, indent=4))
self.window.show_view(Main(self.pypresence_client, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, # temporarily fixes the issue of bad resolution after deletion with less than 2 rows self.window.show_view(Main(self.pypresence_client, self.current_tab, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle))
self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle))
def load_content(self): def load_content(self):
self.tab_content.clear() self.tab_content.clear()
@@ -531,6 +537,12 @@ class Main(arcade.gui.UIView):
elif self.current_mode == "playlist": elif self.current_mode == "playlist":
self.show_content(self.current_tab, "playlist") self.show_content(self.current_tab, "playlist")
def global_search(self):
from menus.global_search import GlobalSearch
arcade.unschedule(self.update_presence)
self.ui.clear()
self.window.show_view(GlobalSearch(self.pypresence_client, self.current_tab, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle))
def settings(self): def settings(self):
from menus.settings import Settings from menus.settings import Settings
arcade.unschedule(self.update_presence) arcade.unschedule(self.update_presence)

View File

@@ -3,34 +3,33 @@ import arcade, arcade.gui, webbrowser
from arcade.gui.experimental.focus import UIFocusGroup from arcade.gui.experimental.focus import UIFocusGroup
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
from utils.online_metadata import get_music_metadata, get_album_cover_art from utils.online_metadata import get_music_metadata, download_albums_cover_art
from utils.constants import button_style from utils.constants import button_style
from utils.preload import button_texture, button_hovered_texture from utils.preload import button_texture, button_hovered_texture
from utils.utils import convert_seconds_to_date from utils.utils import convert_seconds_to_date
from utils.music_handling import convert_timestamp_to_time_ago from utils.music_handling import convert_timestamp_to_time_ago, truncate_end
class MetadataViewer(arcade.gui.UIView): class MetadataViewer(arcade.gui.UIView):
def __init__(self, pypresence_client, metadata_type="music", metadata=None, file_path=None, *args): def __init__(self, pypresence_client, metadata_type="file", metadata=None, file_path=None, *args):
super().__init__() super().__init__()
self.metadata_type = metadata_type self.metadata_type = metadata_type
if metadata_type == "music": if metadata_type == "file":
self.file_metadata = metadata self.file_metadata = metadata
self.artist = self.file_metadata["artist"]
self.file_path = file_path self.file_path = file_path
if self.artist == "Unknown":
self.artist = None self.artist = self.file_metadata["artist"] if not self.file_metadata["artist"] == "Unknown" else None
self.title = self.file_metadata["title"] self.title = self.file_metadata["title"]
self.online_metadata = get_music_metadata(self.artist, self.title) self.music_metadata, self.artist_metadata, self.album_metadata, self.lyrics_metadata = get_music_metadata(self.artist, self.title)
elif metadata_type == "music":
self.artist = metadata["artist"]
self.title = metadata["title"]
self.music_metadata, self.artist_metadata, self.album_metadata, self.lyrics_metadata = get_music_metadata(musicbrainz_id=metadata["id"])
elif metadata_type == "artist": elif metadata_type == "artist":
self.artist_metadata = metadata self.artist_metadata = metadata
elif metadata_type == "album": elif metadata_type == "album":
self.album_metadata = metadata self.album_metadata = metadata
elif metadata_type:
self.artist = metadata["artist"]
self.title = metadata["title"]
self.music_lyrics = metadata["lyrics"]
self.pypresence_client = pypresence_client self.pypresence_client = pypresence_client
self.args = args self.args = args
@@ -42,7 +41,6 @@ class MetadataViewer(arcade.gui.UIView):
self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1))) self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1)))
self.back_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50), anchor_x="left", anchor_y="top", align_x=5, align_y=-5) self.back_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50), anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
self.back_button.on_click = lambda event: self.main_exit()
self.scroll_area = UIScrollArea(size_hint=(0.6, 0.8)) # center on screen self.scroll_area = UIScrollArea(size_hint=(0.6, 0.8)) # center on screen
self.scroll_area.scroll_speed = -50 self.scroll_area.scroll_speed = -50
@@ -52,24 +50,43 @@ class MetadataViewer(arcade.gui.UIView):
self.scrollbar.size_hint = (0.02, 1) self.scrollbar.size_hint = (0.02, 1)
self.anchor.add(self.scrollbar, anchor_x="right", anchor_y="center") self.anchor.add(self.scrollbar, anchor_x="right", anchor_y="center")
self.box = arcade.gui.UIBoxLayout(space_between=10, align='top') self.box = arcade.gui.UIBoxLayout(space_between=10, align='center')
self.scroll_area.add(self.box) self.scroll_area.add(self.box)
self.more_metadata_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="left", anchor_y="bottom", align_x=10, align_y=10) self.more_metadata_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="left", anchor_y="bottom", align_x=10, align_y=10)
if self.metadata_type == "music": self.show_metadata()
tags = ', '.join(self.online_metadata[0]['tags'])
albums = ', '.join(list(self.online_metadata[2].keys())) def show_metadata(self):
name = f"{self.file_metadata['artist']} - {self.file_metadata['title']} Metadata" if self.metadata_type == "file":
metadata_text = f'''File path: {self.file_path} self.back_button.on_click = lambda event: self.main_exit()
File Artist(s): {self.file_metadata['artist']} elif self.metadata_type == "music":
MusicBrainz Artist(s): {', '.join([artist for artist in self.online_metadata[1]])} self.back_button.on_click = lambda event: self.global_search()
Title: {self.file_metadata['title']} else:
MusicBrainz ID: {self.online_metadata[0]['musicbrainz_id']} self.back_button.on_click = lambda event: self.reset_to_music_view()
ISRC(s): {', '.join(self.online_metadata[0]['isrc-list']) if self.online_metadata[0]['isrc-list'] else "None"}
MusicBrainz Rating: {self.online_metadata[0]['musicbrainz_rating']} self.more_metadata_buttons.clear()
self.metadata_labels.clear()
self.box.clear()
self.more_metadata_box.clear()
if self.metadata_type in ["file", "music"]:
tags = ', '.join(self.music_metadata['tags'])
albums = truncate_end(', '.join([album["album_name"] for album in self.album_metadata.values()]), 50)
name = f"{self.artist} - {self.title} Metadata"
musicbrainz_metadata_text = f'''MusicBrainz Artist(s): {', '.join([artist for artist in self.artist_metadata])}
MusicBrainz ID: {self.music_metadata['musicbrainz_id']}
ISRC(s): {', '.join(self.music_metadata['isrc-list']) if self.music_metadata['isrc-list'] else "None"}
MusicBrainz Rating: {self.music_metadata['musicbrainz_rating']}
Tags: {tags if tags else 'None'} Tags: {tags if tags else 'None'}
Albums: {albums if albums else 'None'} Albums: {albums if albums else 'None'}'''
if self.metadata_type == "file":
metadata_text = f'''File path: {self.file_path}
File Artist(s): {self.file_metadata['artist']}
Title: {self.file_metadata['title']}
{musicbrainz_metadata_text}
File size: {self.file_metadata['file_size']}MiB File size: {self.file_metadata['file_size']}MiB
Upload Year: {self.file_metadata['upload_year'] or 'Unknown'} Upload Year: {self.file_metadata['upload_year'] or 'Unknown'}
@@ -77,25 +94,26 @@ Amount of times played: {self.file_metadata['play_count']}
Last Played: {convert_timestamp_to_time_ago(int(self.file_metadata['last_played']))} Last Played: {convert_timestamp_to_time_ago(int(self.file_metadata['last_played']))}
Sound length: {convert_seconds_to_date(int(self.file_metadata['sound_length']))} Sound length: {convert_seconds_to_date(int(self.file_metadata['sound_length']))}
Bitrate: {self.file_metadata['bitrate']}Kbps Bitrate: {self.file_metadata['bitrate']}Kbps
Sample rate: {self.file_metadata['sample_rate']}KHz Sample rate: {self.file_metadata['sample_rate']}KHz'''
''' else:
self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Artist Metadata", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 5.5, height=self.window.height / 15))) metadata_text = musicbrainz_metadata_text
self.more_metadata_buttons[-1].on_click = lambda event: self.window.show_view(MetadataViewer(self.pypresence_client, "artist", self.online_metadata[1], None, *self.args))
self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Album Metadata", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 5.5, height=self.window.height / 15))) metadata_text += f"\n\nLyrics:\n{self.lyrics_metadata}"
self.more_metadata_buttons[-1].on_click = lambda event: self.window.show_view(MetadataViewer(self.pypresence_client, "album", self.online_metadata[2], None, *self.args))
self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Lyrics", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 5.5, height=self.window.height / 15))) self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Artist Metadata", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 4.5 if self.metadata_type == "file" else self.window.width / 2.5, height=self.window.height / 15)))
self.more_metadata_buttons[-1].on_click = lambda event: self.window.show_view(MetadataViewer(self.pypresence_client, "lyrics", {"artist": self.artist, "title": self.title, "lyrics": self.online_metadata[3]}, None, *self.args)) self.more_metadata_buttons[-1].on_click = lambda event: self.show_artist_metadata()
self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Open Uploader URL", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 5.5, height=self.window.height / 15))) self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Album Metadata", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 4.5 if self.metadata_type == "file" else self.window.width / 2.5, height=self.window.height / 15)))
self.more_metadata_buttons[-1].on_click = lambda event: webbrowser.open(self.file_metadata["uploader_url"]) if not self.file_metadata.get("uploader_url", "Unknown") == "Unknown" else None self.more_metadata_buttons[-1].on_click = lambda event: self.show_album_metadata()
self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Open Source URL", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 5.5, height=self.window.height / 15))) if self.metadata_type == "file":
self.more_metadata_buttons[-1].on_click = lambda event: webbrowser.open(self.file_metadata["source_url"]) if not self.file_metadata.get("source_url", "Unknown") == "Unknown" else None self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Open Uploader URL", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 4.5, height=self.window.height / 15)))
self.more_metadata_buttons[-1].on_click = lambda event: webbrowser.open(self.file_metadata["uploader_url"]) if not self.file_metadata.get("uploader_url", "Unknown") == "Unknown" else None
metadata_box = self.box.add(arcade.gui.UIBoxLayout(space_between=10, align='left')) self.more_metadata_buttons.append(self.more_metadata_box.add(arcade.gui.UITextureButton(text="Open Source URL", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 4.5, height=self.window.height / 15)))
self.more_metadata_buttons[-1].on_click = lambda event: webbrowser.open(self.file_metadata["source_url"]) if not self.file_metadata.get("source_url", "Unknown") == "Unknown" else None
metadata_box = self.box.add(arcade.gui.UIBoxLayout(space_between=10, align='center'))
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True))) self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True)))
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=metadata_text, font_size=18, font_name="Roboto", multiline=True))) self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=metadata_text, font_size=18, font_name="Roboto", multiline=True)))
@@ -103,10 +121,12 @@ Sample rate: {self.file_metadata['sample_rate']}KHz
for artist_name, artist_dict in self.artist_metadata.items(): for artist_name, artist_dict in self.artist_metadata.items():
ipi_list = ', '.join(artist_dict['ipi-list']) ipi_list = ', '.join(artist_dict['ipi-list'])
isni_list = ', '.join(artist_dict['isni-list']) isni_list = ', '.join(artist_dict['isni-list'])
tag_list = ','.join(artist_dict['tag-list']) tag_list = ', '.join(artist_dict['tag-list'])
example_tracks = ', '.join(artist_dict['example_tracks'])
name = f"{artist_name} Metadata" name = f"{artist_name} Metadata"
metadata_text = f'''Artist MusicBrainz ID: {artist_dict['musicbrainz_id']} metadata_text = f'''Artist MusicBrainz ID: {artist_dict['musicbrainz_id']}
Artist Gender: {artist_dict['gender']} Artist Gender: {artist_dict['gender']}
Example Tracks: {example_tracks}
Artist Tag(s): {tag_list if tag_list else 'None'} Artist Tag(s): {tag_list if tag_list else 'None'}
Artist IPI(s): {ipi_list if ipi_list else 'None'} Artist IPI(s): {ipi_list if ipi_list else 'None'}
Artist ISNI(s): {isni_list if isni_list else 'None'} Artist ISNI(s): {isni_list if isni_list else 'None'}
@@ -114,7 +134,10 @@ Artist Born: {artist_dict['born']}
Artist Dead: {'Yes' if artist_dict['dead'] else 'No'} Artist Dead: {'Yes' if artist_dict['dead'] else 'No'}
Artist Comment: {artist_dict['comment']} Artist Comment: {artist_dict['comment']}
''' '''
metadata_box = self.box.add(arcade.gui.UIBoxLayout(space_between=10, align='left')) for url_name, url_target in artist_dict["urls"].items():
metadata_text += f"\n{url_name.capitalize()} Links: {', '.join(url_target)}"
metadata_box = self.box.add(arcade.gui.UIBoxLayout(space_between=10, align='center'))
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True))) self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True)))
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=metadata_text, font_size=18, font_name="Roboto", multiline=True))) self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=metadata_text, font_size=18, font_name="Roboto", multiline=True)))
@@ -125,13 +148,16 @@ Artist Comment: {artist_dict['comment']}
self.cover_art_box = self.box.add(arcade.gui.UIBoxLayout(space_between=100, align="left")) self.cover_art_box = self.box.add(arcade.gui.UIBoxLayout(space_between=100, align="left"))
for album_name, album_dict in self.album_metadata.items(): album_cover_arts = download_albums_cover_art([album_id for album_id in self.album_metadata.keys()])
name = f"{album_name} Metadata"
for album_id, album_dict in self.album_metadata.items():
name = f"{album_dict['album_name']} Metadata"
metadata_text = f''' metadata_text = f'''
MusicBrainz Album ID: {album_dict['musicbrainz_id']} MusicBrainz Album ID: {album_id}
Album Name: {album_dict['album_name']} Album Name: {album_dict['album_name']}
Album Date: {album_dict['album_date']} Album Date: {album_dict['album_date']}
Album Country: {album_dict['album_country']} Album Country: {album_dict['album_country']}
Example Tracks: {", ".join(album_dict['album_tracks'])}
''' '''
full_box = self.box.add(arcade.gui.UIBoxLayout(space_between=30, align='center', vertical=False)) full_box = self.box.add(arcade.gui.UIBoxLayout(space_between=30, align='center', vertical=False))
metadata_box = full_box.add(arcade.gui.UIBoxLayout(space_between=10, align='center')) metadata_box = full_box.add(arcade.gui.UIBoxLayout(space_between=10, align='center'))
@@ -139,19 +165,35 @@ Album Country: {album_dict['album_country']}
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True))) self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True)))
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=metadata_text, font_size=18, font_name="Roboto", multiline=True))) self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=metadata_text, font_size=18, font_name="Roboto", multiline=True)))
cover_art = get_album_cover_art(album_dict["musicbrainz_id"]) cover_art = album_cover_arts[album_id]
if cover_art: if cover_art:
full_box.add(arcade.gui.UIImage(texture=cover_art, width=self.window.width / 10, height=self.window.height / 6)) full_box.add(arcade.gui.UIImage(texture=cover_art, width=self.window.width / 10, height=self.window.height / 6))
else: else:
full_box.add(arcade.gui.UILabel(text="No cover found.", font_size=18, font_name="Roboto")) full_box.add(arcade.gui.UILabel(text="No cover found.", font_size=18, font_name="Roboto"))
elif self.metadata_type == "lyrics": def reset_to_music_view(self):
name = f"{self.artist} - {self.title} Lyrics" if hasattr(self, "file_metadata"):
metadata_box = self.box.add(arcade.gui.UIBoxLayout(space_between=10, align='left')) self.metadata_type = "file"
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=name, font_size=20, font_name="Roboto", multiline=True))) elif hasattr(self, "lyrics_metadata"):
self.metadata_labels.append(metadata_box.add(arcade.gui.UILabel(text=self.music_lyrics, font_size=18, font_name="Roboto", multiline=True))) self.metadata_type = "music"
else: # artists and albums from global search
self.global_search()
return
self.show_metadata()
def show_artist_metadata(self):
self.metadata_type = "artist"
self.show_metadata()
def show_album_metadata(self):
self.metadata_type = "album"
self.show_metadata()
def main_exit(self): def main_exit(self):
from menus.main import Main from menus.main import Main
self.window.show_view(Main(self.pypresence_client, *self.args)) self.window.show_view(Main(self.pypresence_client, *self.args))
def global_search(self):
from menus.global_search import GlobalSearch
self.window.show_view(GlobalSearch(self.pypresence_client, *self.args))

View File

@@ -9,6 +9,8 @@ discord_presence_id = 1368277020332523530
audio_extensions = ["mp3", "m4a", "aac", "flac", "ogg", "opus", "wav"] audio_extensions = ["mp3", "m4a", "aac", "flac", "ogg", "opus", "wav"]
view_modes = ["files", "playlist"] view_modes = ["files", "playlist"]
COVER_CACHE_DIR = "cover_cache"
MUSICBRAINZ_PROJECT_NAME = "csd4ni3l/music-player" MUSICBRAINZ_PROJECT_NAME = "csd4ni3l/music-player"
MUSCIBRAINZ_VERSION = "git" MUSCIBRAINZ_VERSION = "git"
MUSICBRAINZ_CONTACT = "csd4ni3l@proton.me" MUSICBRAINZ_CONTACT = "csd4ni3l@proton.me"

View File

@@ -1,13 +1,25 @@
import musicbrainzngs as music_api import musicbrainzngs as music_api
from iso3166 import countries
from utils.constants import MUSICBRAINZ_PROJECT_NAME, MUSICBRAINZ_CONTACT, MUSCIBRAINZ_VERSION from io import BytesIO
import urllib.request, json, os, arcade from PIL import Image
WORD_BLACKLIST = ["compilation", "remix", "vs", "cover"] from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
from utils.constants import MUSICBRAINZ_PROJECT_NAME, MUSICBRAINZ_CONTACT, MUSCIBRAINZ_VERSION, COVER_CACHE_DIR
import urllib.request, json, os, arcade, logging, iso3166
WORD_BLACKLIST = ["compilation", "remix", "vs", "cover", "version", "instrumental", "restrung", "interlude"]
LRCLIB_BASE_URL = "https://lrclib.net/api/search" LRCLIB_BASE_URL = "https://lrclib.net/api/search"
def get_country(code):
country = iso3166.countries.get(code, None)
return country.name if country else "Worldwide"
def check_blacklist(text, blacklist): def check_blacklist(text, blacklist):
return any(word in text for word in blacklist) return any(word in text for word in blacklist)
@@ -20,27 +32,26 @@ def finalize_blacklist(title):
return blacklist return blacklist
def is_release_valid(release_id): def is_release_valid(release):
try: return release.get("release-event-count", 0) == 0 # only include albums
release_data = music_api.get_release_by_id(release_id, includes=["release-groups"])
rg = release_data.get("release", {}).get("release-group", {})
if rg.get("primary-type", "").lower() == "album":
return True
except music_api.ResponseError:
pass
return False
def get_country(country_code): def ensure_metadata_file():
try: if os.path.exists("metadata_cache.json") and os.path.isfile("metadata_cache.json"):
country = countries.get(country_code) with open("metadata_cache.json", "r") as file:
except KeyError: metadata_cache = json.load(file)
country = None else:
metadata_cache = {
"query_results": {},
"recording_by_id": {},
"artist_by_id": {},
"lyrics_by_artist_title": {},
"album_by_id": {}
}
return country.name if country else None return metadata_cache
def get_artists_metadata(artist_ids): def get_artists_metadata(artist_ids):
with open("metadata_cache.json", "r") as file: metadata_cache = ensure_metadata_file()
metadata_cache = json.load(file)
artist_metadata = {} artist_metadata = {}
@@ -50,11 +61,12 @@ def get_artists_metadata(artist_ids):
name = data["name"] name = data["name"]
artist_metadata[name] = data artist_metadata[name] = data
else: else:
artist_data = music_api.get_artist_by_id(artist_id)["artist"] artist_data = music_api.get_artist_by_id(artist_id, includes=["annotation", "releases", "url-rels"])["artist"]
artist_metadata[artist_data["name"]] = { metadata = {
"name": artist_data["name"], "name": artist_data["name"],
"musicbrainz_id": artist_id, "musicbrainz_id": artist_id,
"example_tracks": [release["title"] for release in artist_data.get("release-list", [])[:3]],
"gender": artist_data.get("gender", "Unknown"), "gender": artist_data.get("gender", "Unknown"),
"country": get_country(artist_data.get("country", "WZ")) or "Unknown", "country": get_country(artist_data.get("country", "WZ")) or "Unknown",
"tag-list": [tag["name"] for tag in artist_data.get("tag_list", [])], "tag-list": [tag["name"] for tag in artist_data.get("tag_list", [])],
@@ -62,90 +74,114 @@ def get_artists_metadata(artist_ids):
"isni-list": artist_data.get("isni-list", []), "isni-list": artist_data.get("isni-list", []),
"born": artist_data.get("life-span", {}).get("begin", "Unknown"), "born": artist_data.get("life-span", {}).get("begin", "Unknown"),
"dead": artist_data.get("life-span", {}).get("ended", "Unknown").lower() == "true", "dead": artist_data.get("life-span", {}).get("ended", "Unknown").lower() == "true",
"comment": artist_data.get("disambiguation", "None") "comment": artist_data.get("disambiguation", "None"),
"urls": {}
} }
metadata_cache["artist_by_id"][artist_id] = artist_metadata[artist_data["name"]] for url_data in artist_data.get("url-relation-list", []):
url_type = url_data.get("type", "").lower()
url_target = url_data.get("target", "")
if not url_type or not url_target or not url_type in ["youtube", "imdb", "viaf", "soundcloud", "wikidata", "last.fm", "lyrics", "official homepage"]:
continue
if url_type in metadata["urls"]:
metadata["urls"][url_type].append(url_target)
else:
metadata["urls"][url_type] = [url_target]
artist_metadata[artist_data["name"]] = metadata
metadata_cache["artist_by_id"][artist_id] = metadata
with open("metadata_cache.json", "w") as file: with open("metadata_cache.json", "w") as file:
file.write(json.dumps(metadata_cache)) file.write(json.dumps(metadata_cache))
return artist_metadata return artist_metadata
def get_albums_metadata(release_list): def extract_release_metadata(release_list):
with open("metadata_cache.json", "r") as file: metadata_cache = ensure_metadata_file()
metadata_cache = json.load(file)
album_metadata = {} album_metadata = {}
for release in release_list: for release in release_list:
if not isinstance(release, dict):
continue
release_title = release.get("title", "").lower() release_title = release.get("title", "").lower()
release_id = release["id"]
if any(word in release_title for word in ["single", "ep", "maxi"]): if any(word in release_title for word in ["single", "ep", "maxi"]):
continue continue
if release.get("status") == "Official": if release.get("status") == "Official":
release_id = release["id"] if release_id in metadata_cache["album_by_id"]:
if release_id in metadata_cache["is_release_album_by_id"]: album_metadata[release_id] = metadata_cache["album_by_id"][release_id]
if not metadata_cache["is_release_album_by_id"][release_id]:
continue
else: else:
if not is_release_valid(release_id): album_metadata[release_id] = {
metadata_cache["is_release_album_by_id"][release_id] = False "musicbrainz_id": release.get("id") if release else "Unknown",
continue "album_name": release.get("title") if release else "Unknown",
"album_date": release.get("date") if release else "Unknown",
metadata_cache["is_release_album_by_id"][release_id] = True "album_country": (get_country(release.get("country", "WZ")) or "Worldwide") if release else "Unknown",
}
album_metadata[release.get("title", "")] = { metadata_cache["album_by_id"][release_id] = album_metadata[release_id]
"musicbrainz_id": release.get("id") if release else "Unknown",
"album_name": release.get("title") if release else "Unknown",
"album_date": release.get("date") if release else "Unknown",
"album_country": (get_country(release.get("country", "WZ")) or "Worldwide") if release else "Unknown",
}
with open("metadata_cache.json", "w") as file: with open("metadata_cache.json", "w") as file:
file.write(json.dumps(metadata_cache)) file.write(json.dumps(metadata_cache))
return album_metadata return album_metadata
def get_music_metadata(artist, title): def get_album_metadata(album_id):
if os.path.exists("metadata_cache.json") and os.path.isfile("metadata_cache.json"): metadata_cache = ensure_metadata_file()
with open("metadata_cache.json", "r") as file:
metadata_cache = json.load(file) release = music_api.get_release_by_id(album_id, includes=["recordings"])["release"]
if album_id in metadata_cache["album_by_id"]:
album_metadata = metadata_cache["album_by_id"][release["id"]]
else: else:
metadata_cache = { album_metadata = {
"query_results": {}, "musicbrainz_id": release.get("id") if release else "Unknown",
"recording_by_id": {}, "album_name": release.get("title") if release else "Unknown",
"artist_by_id": {}, "album_date": release.get("date") if release else "Unknown",
"is_release_album_by_id": {}, "album_country": (get_country(release.get("country", "WZ")) or "Worldwide") if release else "Unknown",
"lyrics_by_id": {} "album_tracks": [track['recording']['title'] for track in release.get('medium-list', [])[0].get('track-list', {})[:3]]
} }
metadata_cache["album_by_id"][release["id"]] = album_metadata
with open("metadata_cache.json", "w") as file:
file.write(json.dumps(metadata_cache))
return album_metadata
def get_music_metadata(artist=None, title=None, musicbrainz_id=None):
metadata_cache = ensure_metadata_file()
music_api.set_useragent(MUSICBRAINZ_PROJECT_NAME, MUSCIBRAINZ_VERSION, MUSICBRAINZ_CONTACT) music_api.set_useragent(MUSICBRAINZ_PROJECT_NAME, MUSCIBRAINZ_VERSION, MUSICBRAINZ_CONTACT)
if artist: if not musicbrainz_id:
query = f"{artist} - {title}" if artist:
query = f"{artist} - {title}"
else:
query = title
if query in metadata_cache["query_results"]:
recording_id = metadata_cache["query_results"][query]
else:
results = music_api.search_recordings(query=query, limit=100)["recording-list"]
finalized_blacklist = finalize_blacklist(title)
for r in results:
if not r.get("title") or not r.get("isrc-list"):
continue
if check_blacklist(r["title"].lower(), finalized_blacklist) or check_blacklist(r.get("disambiguation", "").lower(), finalized_blacklist):
continue
recording_id = r["id"]
break
metadata_cache["query_results"][query] = recording_id
else: else:
query = title recording_id = musicbrainz_id
if query in metadata_cache["query_results"]:
recording_id = metadata_cache["query_results"][query]
else:
results = music_api.search_recordings(query=title, limit=100)["recording-list"]
finalized_blacklist = finalize_blacklist(title)
for r in results:
if not r.get("title") or not r.get("isrc-list"):
continue
if check_blacklist(r["title"].lower(), finalized_blacklist) or check_blacklist(r.get("disambiguation", "").lower(), finalized_blacklist):
continue
recording_id = r["id"]
break
metadata_cache["query_results"][query] = recording_id
if recording_id in metadata_cache["recording_by_id"]: if recording_id in metadata_cache["recording_by_id"]:
detailed = metadata_cache["recording_by_id"][recording_id] detailed = metadata_cache["recording_by_id"][recording_id]
@@ -155,27 +191,21 @@ def get_music_metadata(artist, title):
includes=["artists", "releases", "isrcs", "tags", "ratings"] includes=["artists", "releases", "isrcs", "tags", "ratings"]
)["recording"] )["recording"]
metadata_cache["recording_by_id"][recording_id] = { metadata_cache["recording_by_id"][recording_id] = {
"title": detailed["title"],
"artist-credit": [{"artist": {"id": artist_data["artist"]["id"]}} for artist_data in detailed.get("artist-credit", {}) if isinstance(artist_data, dict)], "artist-credit": [{"artist": {"id": artist_data["artist"]["id"]}} for artist_data in detailed.get("artist-credit", {}) if isinstance(artist_data, dict)],
"isrc-list": detailed["isrc-list"] if "isrc-list" in detailed else [], "isrc-list": detailed["isrc-list"] if "isrc-list" in detailed else [],
"rating": {"rating": detailed["rating"]["rating"]} if "rating" in detailed else {}, "rating": {"rating": detailed["rating"]["rating"]} if "rating" in detailed else {},
"tags": detailed.get("tag-list", []), "tags": detailed.get("tag-list", []),
"release-list": [{"id": release["id"], "title": release["title"], "status": release.get("status"), "date": release.get("date"), "country": release.get("country", "WZ")} for release in detailed["release-list"]] if "release-list" in detailed else [] "release-list": [{"id": release["id"], "title": release["title"], "status": release.get("status"), "date": release.get("date"), "country": release.get("country", "WZ")} for release in detailed["release-list"]] if "release-list" in detailed else [],
"release-event-count": detailed.get("release-event-count", 0)
} }
metadata_cache["lyrics_by_id"] = metadata_cache.get("lyrics_by_id", {})
if recording_id in metadata_cache["lyrics_by_id"]:
lyrics = metadata_cache["lyrics_by_id"][recording_id]
else:
lyrics = get_lyrics(artist, title)
metadata_cache["lyrics_by_id"][recording_id] = lyrics
with open("metadata_cache.json", "w") as file: with open("metadata_cache.json", "w") as file:
file.write(json.dumps(metadata_cache)) file.write(json.dumps(metadata_cache))
artist_ids = [artist_data["artist"]["id"] for artist_data in detailed.get("artist-credit", {}) if isinstance(artist_data, dict)] # isinstance is needed, because sometimes & is included as an artist str artist_ids = [artist_data["artist"]["id"] for artist_data in detailed.get("artist-credit", {}) if isinstance(artist_data, dict)] # isinstance is needed, because sometimes & is included as an artist str
artist_metadata = get_artists_metadata(artist_ids) artist_metadata = get_artists_metadata(artist_ids)
album_metadata = get_albums_metadata(detailed.get("release-list", [])) album_metadata = extract_release_metadata(detailed.get("release-list", []))
music_metadata = { music_metadata = {
"musicbrainz_id": recording_id, "musicbrainz_id": recording_id,
@@ -183,39 +213,115 @@ def get_music_metadata(artist, title):
"musicbrainz_rating": detailed["rating"]["rating"] if "rating" in detailed.get("rating", {}) else "Unknown", "musicbrainz_rating": detailed["rating"]["rating"] if "rating" in detailed.get("rating", {}) else "Unknown",
"tags": [tag["name"] for tag in detailed.get("tag-list", [])] "tags": [tag["name"] for tag in detailed.get("tag-list", [])]
} }
return music_metadata, artist_metadata, album_metadata, get_lyrics(', '.join([artist for artist in artist_metadata]), detailed["title"])[0]
return music_metadata, artist_metadata, album_metadata, lyrics
def get_lyrics(artist, title): def get_lyrics(artist, title):
if artist: metadata_cache = ensure_metadata_file()
query = f"{artist} - {title}"
else:
query = title
query_string = urllib.parse.urlencode({"q": query})
full_url = f"{LRCLIB_BASE_URL}?{query_string}"
with urllib.request.urlopen(full_url) as request: if (artist, title) in metadata_cache["lyrics_by_artist_title"]:
data = json.loads(request.read().decode("utf-8")) return metadata_cache["lyrics_by_artist_title"][(artist, title)]
else:
if artist:
query = f"{artist} - {title}"
else:
query = title
for result in data: query_string = urllib.parse.urlencode({"q": query})
if result.get("plainLyrics"): full_url = f"{LRCLIB_BASE_URL}?{query_string}"
return result["plainLyrics"]
with urllib.request.urlopen(full_url) as request:
data = json.loads(request.read().decode("utf-8"))
for result in data:
if result.get("plainLyrics") and result.get("syncedLyrics"):
metadata_cache["lyrics_by_artist_title"][(artist, title)] = (result["plainLyrics"], result["syncedLyrics"])
return (result["plainLyrics"], result["syncedLyrics"])
with open("metadata_cache.json", "w") as file:
file.write(json.dumps(metadata_cache))
if artist: # if there was an artist, it might have been misleading. For example, on Youtube, the uploader might not be the artist. We retry with only title. if artist: # if there was an artist, it might have been misleading. For example, on Youtube, the uploader might not be the artist. We retry with only title.
return get_lyrics(None, title) return get_lyrics(None, title)
def get_album_cover_art(musicbrainz_album_id): def fetch_image_bytes(url):
try: try:
cover_art_bytes = music_api.get_image_front(musicbrainz_album_id, 250) req = Request(url, headers={"User-Agent": "csd4ni3l/music-player/git python-musicbrainzngs/0.7.1 ( csd4ni3l@proton.me )"})
except music_api.ResponseError: with urlopen(req, timeout=10) as resp:
return resp.read()
except (HTTPError, URLError) as e:
logging.debug(f"Error fetching {url}: {e}")
return None return None
with open("music_cover_art.jpg", "wb") as file: def download_cover_art(mb_album_id, size=250):
file.write(cover_art_bytes) path = os.path.join(COVER_CACHE_DIR, f"{mb_album_id}_{size}.png")
if os.path.exists(path):
return mb_album_id, Image.open(path)
url = f"https://coverartarchive.org/release/{mb_album_id}/front-{size}"
img_bytes = fetch_image_bytes(url)
if not img_bytes:
return mb_album_id, None
try:
img = Image.open(BytesIO(img_bytes)).convert("RGBA")
img.save(path)
return mb_album_id, img
except Exception as e:
logging.debug(f"Failed to decode/save image for {mb_album_id}: {e}")
return mb_album_id, None
def download_albums_cover_art(album_ids, size=250, max_workers=5):
music_api.set_useragent(MUSICBRAINZ_PROJECT_NAME, MUSCIBRAINZ_VERSION, MUSICBRAINZ_CONTACT)
os.makedirs(COVER_CACHE_DIR, exist_ok=True)
images = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(download_cover_art, album_id, size) for album_id in album_ids]
for future in as_completed(futures):
album_id, img = future.result()
images[album_id] = arcade.Texture(img) if img else None
return images
def search_recordings(search_term):
music_api.set_useragent(MUSICBRAINZ_PROJECT_NAME, MUSCIBRAINZ_VERSION, MUSICBRAINZ_CONTACT)
results = music_api.search_recordings(query=search_term, limit=100)["recording-list"]
finalized_blacklist = finalize_blacklist(search_term)
output_list = []
for r in results:
if not r.get("title") or not r.get("isrc-list"):
continue
if check_blacklist(r["title"].lower(), finalized_blacklist) or check_blacklist(r.get("disambiguation", "").lower(), finalized_blacklist):
continue
artist_str = ", ".join([artist["name"] for artist in r["artist-credit"] if isinstance(artist, dict)])
output_list.append((artist_str, r["title"], r["id"]))
return output_list
def search_artists(search_term):
music_api.set_useragent(MUSICBRAINZ_PROJECT_NAME, MUSCIBRAINZ_VERSION, MUSICBRAINZ_CONTACT)
texture = arcade.load_texture("music_cover_art.jpg") results = music_api.search_artists(query=search_term)
os.remove("music_cover_art.jpg") output_list = []
return texture for r in results["artist-list"]:
output_list.append((r["name"], r["id"]))
return output_list
def search_albums(search_term):
music_api.set_useragent(MUSICBRAINZ_PROJECT_NAME, MUSCIBRAINZ_VERSION, MUSICBRAINZ_CONTACT)
results = music_api.search_releases(search_term)
output_list = []
for r in results["release-list"]:
artist_str = ", ".join([artist["name"] for artist in r["artist-credit"] if isinstance(artist, dict)])
output_list.append((artist_str, r["title"], r["id"]))
return output_list

View File

@@ -15,7 +15,10 @@ forward_icon = arcade.load_texture("assets/graphics/forward.png")
backwards_icon = arcade.load_texture("assets/graphics/backwards.png") backwards_icon = arcade.load_texture("assets/graphics/backwards.png")
volume_icon = arcade.load_texture("assets/graphics/volume.png") volume_icon = arcade.load_texture("assets/graphics/volume.png")
person_icon = arcade.load_texture("assets/graphics/person.png")
music_icon = arcade.load_texture("assets/graphics/music.png")
global_search_icon = arcade.load_texture("assets/graphics/global_search.png")
settings_icon = arcade.load_texture("assets/graphics/settings.png") settings_icon = arcade.load_texture("assets/graphics/settings.png")
download_icon = arcade.load_texture("assets/graphics/download.png") download_icon = arcade.load_texture("assets/graphics/download.png")
metadata_icon = arcade.load_texture("assets/graphics/metadata.png") metadata_icon = arcade.load_texture("assets/graphics/metadata.png")
music_icon = arcade.load_texture("assets/graphics/music.png")

View File

@@ -1,9 +1,6 @@
import logging, sys, traceback import logging, sys, traceback, pyglet, arcade, arcade.gui, textwrap
from utils.constants import menu_background_color from utils.constants import menu_background_color
from utils.preload import resume_icon, music_icon
import pyglet, arcade, arcade.gui
from arcade.gui.experimental.scroll_area import UIScrollArea from arcade.gui.experimental.scroll_area import UIScrollArea
@@ -100,22 +97,21 @@ class MouseAwareScrollArea(UIScrollArea):
return super().on_event(event) return super().on_event(event)
class MusicItem(arcade.gui.UIBoxLayout): class Card(arcade.gui.UIBoxLayout):
def __init__(self, metadata: dict, width: int, height: int, padding=10): def __init__(self, thumbnail, line_1: str, line_2: str, width: int, height: int, padding=10):
super().__init__(width=width, height=height, space_between=padding, align="top") super().__init__(width=width, height=height, space_between=padding, align="top")
self.metadata = metadata self.button = self.add(arcade.gui.UITextureButton(
if metadata: texture=thumbnail,
self.button = self.add(arcade.gui.UITextureButton( texture_hovered=thumbnail,
texture=metadata["thumbnail"], width=width / 2.5,
texture_hovered=metadata["thumbnail"], height=height / 2.5,
width=width / 1.25, interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT]
height=height * 0.5, ))
interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT]
))
self.title_label = self.add(arcade.gui.UILabel( if line_1:
text=metadata["title"], self.line_1_label = self.add(arcade.gui.UILabel(
text=line_1,
font_name="Roboto", font_name="Roboto",
font_size=14, font_size=14,
width=width, width=width,
@@ -123,8 +119,9 @@ class MusicItem(arcade.gui.UIBoxLayout):
multiline=True multiline=True
)) ))
self.artist_label = self.add(arcade.gui.UILabel( if line_2:
text=metadata["artist"], self.line_2_label = self.add(arcade.gui.UILabel(
text=line_2,
font_name="Roboto", font_name="Roboto",
font_size=12, font_size=12,
width=width, width=width,
@@ -133,54 +130,24 @@ class MusicItem(arcade.gui.UIBoxLayout):
text_color=arcade.color.GRAY text_color=arcade.color.GRAY
)) ))
self.play_button = self.add(arcade.gui.UITextureButton(
texture=resume_icon,
texture_hovered=resume_icon,
width=width / 10,
height=height / 5,
interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT]
))
self.play_button.visible = False
else:
self.button = self.add(arcade.gui.UITextureButton(
texture=music_icon,
texture_hovered=music_icon,
width=width / 1.25,
height=height * 0.5,
))
self.add_music_label = self.add(arcade.gui.UILabel(
text="Add Music",
font_name="Roboto",
font_size=14,
width=width,
height=height * 0.5,
multiline=True
))
def on_event(self, event: arcade.gui.UIEvent): def on_event(self, event: arcade.gui.UIEvent):
if self.metadata: if isinstance(event, UIMouseOutOfAreaEvent):
if isinstance(event, UIMouseOutOfAreaEvent): # not hovering
self.with_background(color=arcade.color.TRANSPARENT_BLACK)
self.trigger_full_render()
elif isinstance(event, arcade.gui.UIMouseMovementEvent):
if self.rect.point_in_rect(event.pos):
# hovering
self.with_background(color=arcade.color.DARK_GRAY)
self.trigger_full_render()
else:
# not hovering # not hovering
self.with_background(color=arcade.color.TRANSPARENT_BLACK) self.with_background(color=arcade.color.TRANSPARENT_BLACK)
self.play_button.visible = False
self.trigger_full_render() self.trigger_full_render()
elif isinstance(event, arcade.gui.UIMouseMovementEvent): elif isinstance(event, arcade.gui.UIMousePressEvent) and self.rect.point_in_rect(event.pos):
if self.rect.point_in_rect(event.pos): self.button.on_click(event)
# hovering
self.with_background(color=arcade.color.DARK_GRAY)
self.play_button.visible = True
self.trigger_full_render()
else:
# not hovering
self.with_background(color=arcade.color.TRANSPARENT_BLACK)
self.play_button.visible = False
self.trigger_full_render()
elif isinstance(event, arcade.gui.UIMousePressEvent) and self.rect.point_in_rect(event.pos):
self.button.on_click(event)
return super().on_event(event) return super().on_event(event)
@@ -219,4 +186,14 @@ def convert_seconds_to_date(seconds):
if seconds > 0 or not any([days, hours, minutes]): if seconds > 0 or not any([days, hours, minutes]):
result += "{} seconds".format(int(seconds)) result += "{} seconds".format(int(seconds))
return result.strip() return result.strip()
def get_wordwrapped_text(text, width=18):
if len(text) < width:
output_text = text.center(width)
elif len(text) == width:
output_text = text
else:
output_text = '\n'.join(textwrap.wrap(text, width=width))
return output_text