mirror of
https://github.com/csd4ni3l/music-player.git
synced 2025-11-05 02:58:15 +01:00
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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -181,4 +181,5 @@ logs/
|
||||
logs
|
||||
settings.json
|
||||
bin/
|
||||
metadata_cache.json
|
||||
metadata_cache.json
|
||||
cover_cache/
|
||||
BIN
assets/graphics/global_search.png
Normal file
BIN
assets/graphics/global_search.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/graphics/person.png
Normal file
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
117
menus/global_search.py
Normal 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))
|
||||
@@ -3,7 +3,7 @@ import arcade, pyglet
|
||||
|
||||
from utils.preload import *
|
||||
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.file_watching import watch_directories, watch_files
|
||||
|
||||
@@ -123,7 +123,7 @@ class Main(arcade.gui.UIView):
|
||||
self.scrollbar.size_hint = (0.02, 1)
|
||||
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)
|
||||
|
||||
# Controls
|
||||
@@ -179,7 +179,10 @@ class Main(arcade.gui.UIView):
|
||||
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.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.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):
|
||||
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):
|
||||
for music_button in self.music_buttons.values():
|
||||
@@ -309,8 +312,12 @@ class Main(arcade.gui.UIView):
|
||||
|
||||
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)
|
||||
self.highest_score_file = f"{self.current_tab}/{matches[0][0]}"
|
||||
content_to_show = [match[0] for match in matches]
|
||||
if 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:
|
||||
self.highest_score_file = ""
|
||||
@@ -321,8 +328,8 @@ class Main(arcade.gui.UIView):
|
||||
row, col = 0, 0
|
||||
|
||||
for n, music_filename in enumerate(content_to_show):
|
||||
row = n // 7
|
||||
col = n % 7
|
||||
row = n // 8
|
||||
col = n % 8
|
||||
|
||||
if self.current_mode == "files":
|
||||
music_path = f"{tab}/{music_filename}"
|
||||
@@ -331,17 +338,17 @@ class Main(arcade.gui.UIView):
|
||||
|
||||
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)
|
||||
|
||||
row = (n + 1) // 7
|
||||
col = (n + 1) % 7
|
||||
row = (n + 1) // 8
|
||||
col = (n + 1) % 8
|
||||
|
||||
self.music_grid.row_count = row + 1
|
||||
self.music_grid._update_size_hints()
|
||||
|
||||
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.anchor.detect_focusable_widgets()
|
||||
@@ -358,8 +365,7 @@ class Main(arcade.gui.UIView):
|
||||
with open("settings.json", "w") as file:
|
||||
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.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle))
|
||||
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))
|
||||
|
||||
def load_content(self):
|
||||
self.tab_content.clear()
|
||||
@@ -531,6 +537,12 @@ class Main(arcade.gui.UIView):
|
||||
elif self.current_mode == "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):
|
||||
from menus.settings import Settings
|
||||
arcade.unschedule(self.update_presence)
|
||||
|
||||
@@ -3,34 +3,33 @@ import arcade, arcade.gui, webbrowser
|
||||
from arcade.gui.experimental.focus import UIFocusGroup
|
||||
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.preload import button_texture, button_hovered_texture
|
||||
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):
|
||||
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__()
|
||||
self.metadata_type = metadata_type
|
||||
if metadata_type == "music":
|
||||
if metadata_type == "file":
|
||||
self.file_metadata = metadata
|
||||
self.artist = self.file_metadata["artist"]
|
||||
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.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":
|
||||
self.artist_metadata = metadata
|
||||
elif metadata_type == "album":
|
||||
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.args = args
|
||||
@@ -42,7 +41,6 @@ class MetadataViewer(arcade.gui.UIView):
|
||||
|
||||
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.on_click = lambda event: self.main_exit()
|
||||
|
||||
self.scroll_area = UIScrollArea(size_hint=(0.6, 0.8)) # center on screen
|
||||
self.scroll_area.scroll_speed = -50
|
||||
@@ -52,24 +50,43 @@ class MetadataViewer(arcade.gui.UIView):
|
||||
self.scrollbar.size_hint = (0.02, 1)
|
||||
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.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":
|
||||
tags = ', '.join(self.online_metadata[0]['tags'])
|
||||
albums = ', '.join(list(self.online_metadata[2].keys()))
|
||||
name = f"{self.file_metadata['artist']} - {self.file_metadata['title']} Metadata"
|
||||
metadata_text = f'''File path: {self.file_path}
|
||||
File Artist(s): {self.file_metadata['artist']}
|
||||
MusicBrainz Artist(s): {', '.join([artist for artist in self.online_metadata[1]])}
|
||||
Title: {self.file_metadata['title']}
|
||||
MusicBrainz ID: {self.online_metadata[0]['musicbrainz_id']}
|
||||
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.show_metadata()
|
||||
|
||||
def show_metadata(self):
|
||||
if self.metadata_type == "file":
|
||||
self.back_button.on_click = lambda event: self.main_exit()
|
||||
elif self.metadata_type == "music":
|
||||
self.back_button.on_click = lambda event: self.global_search()
|
||||
else:
|
||||
self.back_button.on_click = lambda event: self.reset_to_music_view()
|
||||
|
||||
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'}
|
||||
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
|
||||
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']))}
|
||||
Sound length: {convert_seconds_to_date(int(self.file_metadata['sound_length']))}
|
||||
Bitrate: {self.file_metadata['bitrate']}Kbps
|
||||
Sample rate: {self.file_metadata['sample_rate']}KHz
|
||||
'''
|
||||
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)))
|
||||
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))
|
||||
Sample rate: {self.file_metadata['sample_rate']}KHz'''
|
||||
else:
|
||||
metadata_text = musicbrainz_metadata_text
|
||||
|
||||
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)))
|
||||
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))
|
||||
metadata_text += f"\n\nLyrics:\n{self.lyrics_metadata}"
|
||||
|
||||
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[-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.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.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[-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.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: 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)))
|
||||
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
|
||||
if self.metadata_type == "file":
|
||||
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=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():
|
||||
ipi_list = ', '.join(artist_dict['ipi-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"
|
||||
metadata_text = f'''Artist MusicBrainz ID: {artist_dict['musicbrainz_id']}
|
||||
Artist Gender: {artist_dict['gender']}
|
||||
Example Tracks: {example_tracks}
|
||||
Artist Tag(s): {tag_list if tag_list else 'None'}
|
||||
Artist IPI(s): {ipi_list if ipi_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 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=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"))
|
||||
|
||||
for album_name, album_dict in self.album_metadata.items():
|
||||
name = f"{album_name} Metadata"
|
||||
album_cover_arts = download_albums_cover_art([album_id for album_id in self.album_metadata.keys()])
|
||||
|
||||
for album_id, album_dict in self.album_metadata.items():
|
||||
name = f"{album_dict['album_name']} Metadata"
|
||||
metadata_text = f'''
|
||||
MusicBrainz Album ID: {album_dict['musicbrainz_id']}
|
||||
MusicBrainz Album ID: {album_id}
|
||||
Album Name: {album_dict['album_name']}
|
||||
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))
|
||||
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=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:
|
||||
full_box.add(arcade.gui.UIImage(texture=cover_art, width=self.window.width / 10, height=self.window.height / 6))
|
||||
else:
|
||||
full_box.add(arcade.gui.UILabel(text="No cover found.", font_size=18, font_name="Roboto"))
|
||||
|
||||
elif self.metadata_type == "lyrics":
|
||||
name = f"{self.artist} - {self.title} Lyrics"
|
||||
metadata_box = self.box.add(arcade.gui.UIBoxLayout(space_between=10, align='left'))
|
||||
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=self.music_lyrics, font_size=18, font_name="Roboto", multiline=True)))
|
||||
def reset_to_music_view(self):
|
||||
if hasattr(self, "file_metadata"):
|
||||
self.metadata_type = "file"
|
||||
elif hasattr(self, "lyrics_metadata"):
|
||||
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):
|
||||
from menus.main import Main
|
||||
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))
|
||||
@@ -9,6 +9,8 @@ discord_presence_id = 1368277020332523530
|
||||
audio_extensions = ["mp3", "m4a", "aac", "flac", "ogg", "opus", "wav"]
|
||||
view_modes = ["files", "playlist"]
|
||||
|
||||
COVER_CACHE_DIR = "cover_cache"
|
||||
|
||||
MUSICBRAINZ_PROJECT_NAME = "csd4ni3l/music-player"
|
||||
MUSCIBRAINZ_VERSION = "git"
|
||||
MUSICBRAINZ_CONTACT = "csd4ni3l@proton.me"
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
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"
|
||||
|
||||
def get_country(code):
|
||||
country = iso3166.countries.get(code, None)
|
||||
return country.name if country else "Worldwide"
|
||||
|
||||
def check_blacklist(text, blacklist):
|
||||
return any(word in text for word in blacklist)
|
||||
|
||||
@@ -20,27 +32,26 @@ def finalize_blacklist(title):
|
||||
|
||||
return blacklist
|
||||
|
||||
def is_release_valid(release_id):
|
||||
try:
|
||||
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 is_release_valid(release):
|
||||
return release.get("release-event-count", 0) == 0 # only include albums
|
||||
|
||||
def get_country(country_code):
|
||||
try:
|
||||
country = countries.get(country_code)
|
||||
except KeyError:
|
||||
country = None
|
||||
def ensure_metadata_file():
|
||||
if os.path.exists("metadata_cache.json") and os.path.isfile("metadata_cache.json"):
|
||||
with open("metadata_cache.json", "r") as file:
|
||||
metadata_cache = json.load(file)
|
||||
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):
|
||||
with open("metadata_cache.json", "r") as file:
|
||||
metadata_cache = json.load(file)
|
||||
metadata_cache = ensure_metadata_file()
|
||||
|
||||
artist_metadata = {}
|
||||
|
||||
@@ -50,11 +61,12 @@ def get_artists_metadata(artist_ids):
|
||||
name = data["name"]
|
||||
artist_metadata[name] = data
|
||||
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"],
|
||||
"musicbrainz_id": artist_id,
|
||||
"example_tracks": [release["title"] for release in artist_data.get("release-list", [])[:3]],
|
||||
"gender": artist_data.get("gender", "Unknown"),
|
||||
"country": get_country(artist_data.get("country", "WZ")) or "Unknown",
|
||||
"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", []),
|
||||
"born": artist_data.get("life-span", {}).get("begin", "Unknown"),
|
||||
"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:
|
||||
file.write(json.dumps(metadata_cache))
|
||||
|
||||
return artist_metadata
|
||||
|
||||
def get_albums_metadata(release_list):
|
||||
with open("metadata_cache.json", "r") as file:
|
||||
metadata_cache = json.load(file)
|
||||
def extract_release_metadata(release_list):
|
||||
metadata_cache = ensure_metadata_file()
|
||||
|
||||
album_metadata = {}
|
||||
|
||||
for release in release_list:
|
||||
if not isinstance(release, dict):
|
||||
continue
|
||||
|
||||
release_title = release.get("title", "").lower()
|
||||
release_id = release["id"]
|
||||
|
||||
if any(word in release_title for word in ["single", "ep", "maxi"]):
|
||||
continue
|
||||
|
||||
if release.get("status") == "Official":
|
||||
release_id = release["id"]
|
||||
if release_id in metadata_cache["is_release_album_by_id"]:
|
||||
if not metadata_cache["is_release_album_by_id"][release_id]:
|
||||
continue
|
||||
if release_id in metadata_cache["album_by_id"]:
|
||||
album_metadata[release_id] = metadata_cache["album_by_id"][release_id]
|
||||
else:
|
||||
if not is_release_valid(release_id):
|
||||
metadata_cache["is_release_album_by_id"][release_id] = False
|
||||
continue
|
||||
|
||||
metadata_cache["is_release_album_by_id"][release_id] = True
|
||||
|
||||
album_metadata[release.get("title", "")] = {
|
||||
"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",
|
||||
}
|
||||
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",
|
||||
}
|
||||
metadata_cache["album_by_id"][release_id] = album_metadata[release_id]
|
||||
|
||||
with open("metadata_cache.json", "w") as file:
|
||||
file.write(json.dumps(metadata_cache))
|
||||
|
||||
return album_metadata
|
||||
|
||||
def get_music_metadata(artist, title):
|
||||
if os.path.exists("metadata_cache.json") and os.path.isfile("metadata_cache.json"):
|
||||
with open("metadata_cache.json", "r") as file:
|
||||
metadata_cache = json.load(file)
|
||||
def get_album_metadata(album_id):
|
||||
metadata_cache = ensure_metadata_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:
|
||||
metadata_cache = {
|
||||
"query_results": {},
|
||||
"recording_by_id": {},
|
||||
"artist_by_id": {},
|
||||
"is_release_album_by_id": {},
|
||||
"lyrics_by_id": {}
|
||||
album_metadata = {
|
||||
"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",
|
||||
"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)
|
||||
|
||||
if artist:
|
||||
query = f"{artist} - {title}"
|
||||
if not musicbrainz_id:
|
||||
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:
|
||||
query = title
|
||||
|
||||
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
|
||||
recording_id = musicbrainz_id
|
||||
|
||||
if recording_id in metadata_cache["recording_by_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"]
|
||||
)["recording"]
|
||||
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)],
|
||||
"isrc-list": detailed["isrc-list"] if "isrc-list" in detailed else [],
|
||||
"rating": {"rating": detailed["rating"]["rating"]} if "rating" in detailed else {},
|
||||
"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:
|
||||
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_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 = {
|
||||
"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",
|
||||
"tags": [tag["name"] for tag in detailed.get("tag-list", [])]
|
||||
}
|
||||
|
||||
return music_metadata, artist_metadata, album_metadata, lyrics
|
||||
return music_metadata, artist_metadata, album_metadata, get_lyrics(', '.join([artist for artist in artist_metadata]), detailed["title"])[0]
|
||||
|
||||
def get_lyrics(artist, title):
|
||||
if artist:
|
||||
query = f"{artist} - {title}"
|
||||
else:
|
||||
query = title
|
||||
|
||||
query_string = urllib.parse.urlencode({"q": query})
|
||||
full_url = f"{LRCLIB_BASE_URL}?{query_string}"
|
||||
metadata_cache = ensure_metadata_file()
|
||||
|
||||
with urllib.request.urlopen(full_url) as request:
|
||||
data = json.loads(request.read().decode("utf-8"))
|
||||
if (artist, title) in metadata_cache["lyrics_by_artist_title"]:
|
||||
return metadata_cache["lyrics_by_artist_title"][(artist, title)]
|
||||
else:
|
||||
if artist:
|
||||
query = f"{artist} - {title}"
|
||||
else:
|
||||
query = title
|
||||
|
||||
for result in data:
|
||||
if result.get("plainLyrics"):
|
||||
return result["plainLyrics"]
|
||||
query_string = urllib.parse.urlencode({"q": query})
|
||||
full_url = f"{LRCLIB_BASE_URL}?{query_string}"
|
||||
|
||||
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.
|
||||
return get_lyrics(None, title)
|
||||
|
||||
def get_album_cover_art(musicbrainz_album_id):
|
||||
def fetch_image_bytes(url):
|
||||
try:
|
||||
cover_art_bytes = music_api.get_image_front(musicbrainz_album_id, 250)
|
||||
except music_api.ResponseError:
|
||||
req = Request(url, headers={"User-Agent": "csd4ni3l/music-player/git python-musicbrainzngs/0.7.1 ( csd4ni3l@proton.me )"})
|
||||
with urlopen(req, timeout=10) as resp:
|
||||
return resp.read()
|
||||
except (HTTPError, URLError) as e:
|
||||
logging.debug(f"Error fetching {url}: {e}")
|
||||
return None
|
||||
|
||||
with open("music_cover_art.jpg", "wb") as file:
|
||||
file.write(cover_art_bytes)
|
||||
def download_cover_art(mb_album_id, size=250):
|
||||
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
|
||||
@@ -15,7 +15,10 @@ forward_icon = arcade.load_texture("assets/graphics/forward.png")
|
||||
backwards_icon = arcade.load_texture("assets/graphics/backwards.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")
|
||||
download_icon = arcade.load_texture("assets/graphics/download.png")
|
||||
metadata_icon = arcade.load_texture("assets/graphics/metadata.png")
|
||||
music_icon = arcade.load_texture("assets/graphics/music.png")
|
||||
103
utils/utils.py
103
utils/utils.py
@@ -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.preload import resume_icon, music_icon
|
||||
|
||||
import pyglet, arcade, arcade.gui
|
||||
|
||||
from arcade.gui.experimental.scroll_area import UIScrollArea
|
||||
|
||||
@@ -100,22 +97,21 @@ class MouseAwareScrollArea(UIScrollArea):
|
||||
|
||||
return super().on_event(event)
|
||||
|
||||
class MusicItem(arcade.gui.UIBoxLayout):
|
||||
def __init__(self, metadata: dict, width: int, height: int, padding=10):
|
||||
class Card(arcade.gui.UIBoxLayout):
|
||||
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")
|
||||
|
||||
self.metadata = metadata
|
||||
if metadata:
|
||||
self.button = self.add(arcade.gui.UITextureButton(
|
||||
texture=metadata["thumbnail"],
|
||||
texture_hovered=metadata["thumbnail"],
|
||||
width=width / 1.25,
|
||||
height=height * 0.5,
|
||||
interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT]
|
||||
))
|
||||
self.button = self.add(arcade.gui.UITextureButton(
|
||||
texture=thumbnail,
|
||||
texture_hovered=thumbnail,
|
||||
width=width / 2.5,
|
||||
height=height / 2.5,
|
||||
interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT]
|
||||
))
|
||||
|
||||
self.title_label = self.add(arcade.gui.UILabel(
|
||||
text=metadata["title"],
|
||||
if line_1:
|
||||
self.line_1_label = self.add(arcade.gui.UILabel(
|
||||
text=line_1,
|
||||
font_name="Roboto",
|
||||
font_size=14,
|
||||
width=width,
|
||||
@@ -123,8 +119,9 @@ class MusicItem(arcade.gui.UIBoxLayout):
|
||||
multiline=True
|
||||
))
|
||||
|
||||
self.artist_label = self.add(arcade.gui.UILabel(
|
||||
text=metadata["artist"],
|
||||
if line_2:
|
||||
self.line_2_label = self.add(arcade.gui.UILabel(
|
||||
text=line_2,
|
||||
font_name="Roboto",
|
||||
font_size=12,
|
||||
width=width,
|
||||
@@ -133,54 +130,24 @@ class MusicItem(arcade.gui.UIBoxLayout):
|
||||
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):
|
||||
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
|
||||
self.with_background(color=arcade.color.TRANSPARENT_BLACK)
|
||||
self.play_button.visible = False
|
||||
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.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)
|
||||
elif isinstance(event, arcade.gui.UIMousePressEvent) and self.rect.point_in_rect(event.pos):
|
||||
self.button.on_click(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]):
|
||||
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
|
||||
Reference in New Issue
Block a user