Add a view metadata button, add more metadata to file after download and extract more from files.

This commit is contained in:
csd4ni3l
2025-06-26 22:00:29 +02:00
parent b192bdf424
commit ad51f1236f
5 changed files with 148 additions and 106 deletions

View File

@@ -1,6 +1,7 @@
from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3, TIT2, TPE1, WXXX
from mutagen.mp3 import MP3
import arcade, arcade.gui, os, json, threading, subprocess import arcade, arcade.gui, os, json, threading, subprocess, traceback
from arcade.gui.experimental.focus import UIFocusGroup from arcade.gui.experimental.focus import UIFocusGroup
@@ -122,9 +123,18 @@ class Downloader(arcade.gui.UIView):
title = f"{artist} - {track_title}" title = f"{artist} - {track_title}"
try: try:
audio = EasyID3("downloaded_music.mp3") audio = MP3("downloaded_music.mp3", ID3=ID3)
audio["artist"] = artist if audio.tags is None:
audio["title"] = track_title audio.add_tags()
else:
for frame_id in ("TIT2", "TPE1", "WXXX"):
audio.tags.delall(frame_id)
audio.tags.add(TIT2(encoding=3, text=track_title))
audio.tags.add(TPE1(encoding=3, text=artist))
if info.get("creator_url"):
audio.tags.add(WXXX(desc="Uploader", url=info["uploader_url"]))
audio.tags.add(WXXX(desc="Source", url=info["webpage_url"]))
audio.save() audio.save()
except Exception as meta_err: except Exception as meta_err:
self.yt_dl_buffer = f"ERROR: Tried to override metadata based on title, but failed: {meta_err}" self.yt_dl_buffer = f"ERROR: Tried to override metadata based on title, but failed: {meta_err}"

View File

@@ -1,11 +1,10 @@
import random, asyncio, pypresence, time, copy, json, os, logging import random, asyncio, pypresence, time, copy, json, os, logging, webbrowser
import arcade, pyglet 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, ListItem, extract_metadata, get_audio_thumbnail_texture, truncate_end, adjust_volume from utils.utils import FakePyPresence, UIFocusTextureButton, MusicItem, extract_metadata_and_thumbnail, truncate_end, adjust_volume
from math import ceil
from thefuzz import process, fuzz from thefuzz import process, fuzz
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
@@ -57,6 +56,7 @@ class Main(arcade.gui.UIView):
self.tab_options = self.settings_dict.get("tab_options", [os.path.join("~", "Music"), os.path.join("~", "Downloads")]) self.tab_options = self.settings_dict.get("tab_options", [os.path.join("~", "Music"), os.path.join("~", "Downloads")])
self.tab_content = {} self.tab_content = {}
self.playlist_content = {} self.playlist_content = {}
self.file_metadata = {}
self.thumbnails = {} self.thumbnails = {}
self.tab_buttons = {} self.tab_buttons = {}
self.music_buttons = {} self.music_buttons = {}
@@ -257,10 +257,22 @@ class Main(arcade.gui.UIView):
self.shuffle = not self.shuffle self.shuffle = not self.shuffle
self.update_buttons() self.update_buttons()
def metadata_button_action(self, action, metadata):
if action != "Close":
webbrowser.open(metadata["uploader_url"] if action == "Uploader" else metadata["source_url"])
def open_metadata(self, file_path):
metadata = self.file_metadata[file_path]
metadata_text = f"File path: {file_path}\nArtist: {metadata['artist']}\nTitle: {metadata['title']}\nSound length: {int(metadata['sound_length'])}\nBitrate: {metadata['bit_rate']}Kbps"
msgbox = arcade.gui.UIMessageBox(title=f"{metadata['artist']} - {metadata['title']} Metadata", buttons=("Uploader", "Source", "Close"), message_text=metadata_text, width=self.window.width / 2, height=self.window.height / 2)
msgbox.on_action = lambda event, metadata=metadata: self.metadata_button_action(event.action, metadata)
self.anchor.add(msgbox, anchor_x="center", anchor_y="center")
def show_content(self, tab): def show_content(self, tab):
for music_button in self.music_buttons.values(): for music_button in self.music_buttons.values():
music_button.remove(music_button.button) music_button.clear()
music_button.remove(music_button.image)
self.music_box.remove(music_button) self.music_box.remove(music_button)
del music_button del music_button
@@ -272,10 +284,13 @@ class Main(arcade.gui.UIView):
if not self.search_term == "": if not self.search_term == "":
matches = process.extract(self.search_term, self.tab_content[self.current_tab], limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio) matches = process.extract(self.search_term, self.tab_content[self.current_tab], limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio)
self.highest_score_file = f"{self.current_tab}/{matches[0][0]}" self.highest_score_file = f"{self.current_tab}/{matches[0][0]}"
for match in matches: for match in matches:
music_filename = match[0] music_filename = match[0]
self.music_buttons[f"{tab}/{music_filename}"] = self.music_box.add(ListItem(texture=self.thumbnails[f"{tab}/{music_filename}"], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 1.2, height=self.window.height / 11)) metadata = self.file_metadata[f"{tab}/{music_filename}"]
self.music_buttons[f"{tab}/{music_filename}"].button.on_click = lambda event, tab=tab, music_filename=music_filename: self.music_button_click(event, f"{tab}/{music_filename}") self.music_buttons[f"{tab}/{music_filename}"] = self.music_box.add(MusicItem(metadata=metadata, texture=self.thumbnails[f"{tab}/{music_filename}"], width=self.window.width / 1.2, height=self.window.height / 11))
self.music_buttons[f"{tab}/{music_filename}"].button.on_click = lambda event, music_path=f"{tab}/{music_filename}": self.music_button_click(event, music_path)
self.music_buttons[f"{tab}/{music_filename}"].view_metadata_button.on_click = lambda event, music_path=f"{tab}/{music_filename}": self.open_metadata(music_path)
else: else:
self.highest_score_file = "" self.highest_score_file = ""
@@ -283,10 +298,11 @@ class Main(arcade.gui.UIView):
self.no_music_label.visible = not self.tab_content[tab] self.no_music_label.visible = not self.tab_content[tab]
for music_filename in self.tab_content[tab]: for music_filename in self.tab_content[tab]:
self.music_buttons[f"{tab}/{music_filename}"] = self.music_box.add(ListItem(texture=self.thumbnails[f"{tab}/{music_filename}"], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 1.2, height=self.window.height / 11)) metadata = self.file_metadata[f"{tab}/{music_filename}"]
self.music_buttons[f"{tab}/{music_filename}"].button.on_click = lambda event, tab=tab, music_filename=music_filename: self.music_button_click(event, f"{tab}/{music_filename}")
self.music_box._update_size_hints() self.music_buttons[f"{tab}/{music_filename}"] = self.music_box.add(MusicItem(metadata=metadata, texture=self.thumbnails[f"{tab}/{music_filename}"], width=self.window.width / 1.2, height=self.window.height / 11))
self.music_buttons[f"{tab}/{music_filename}"].button.on_click = lambda event, music_path=f"{tab}/{music_filename}": self.music_button_click(event, music_path)
self.music_buttons[f"{tab}/{music_filename}"].view_metadata_button.on_click = lambda event, music_path=f"{tab}/{music_filename}": self.open_metadata(music_path)
elif self.current_mode == "playlist": elif self.current_mode == "playlist":
self.current_playlist = tab self.current_playlist = tab
@@ -296,22 +312,24 @@ class Main(arcade.gui.UIView):
matches = process.extract(self.search_term, self.playlist_content[tab], limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio) matches = process.extract(self.search_term, self.playlist_content[tab], limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio)
self.highest_score_file = matches[0][0] self.highest_score_file = matches[0][0]
for match in matches: for match in matches:
music_filename = match[0] music_path = match[0]
self.music_buttons[music_filename] = self.music_box.add(ListItem(texture=self.thumbnails[music_filename], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 1.2, height=self.window.height / 11)) metadata = self.file_metadata[music_path]
self.music_buttons[music_filename].button.on_click = lambda event, music_filename=music_filename: self.music_button_click(event, music_filename) self.music_buttons[music_path] = self.music_box.add(MusicItem(metadata=metadata, texture=self.thumbnails[music_path], width=self.window.width / 1.2, height=self.window.height / 11))
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].view_metadata_button.on_click = lambda event, music_path=music_path: self.open_metadata(music_path)
else: else:
self.highest_score_file = "" self.highest_score_file = ""
self.no_music_label.visible = not self.playlist_content[tab] self.no_music_label.visible = not self.playlist_content[tab]
for music_filename in self.playlist_content[tab]: for music_path in self.playlist_content[tab]:
self.music_buttons[music_filename] = self.music_box.add(ListItem(texture=self.thumbnails[music_filename], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 1.2, height=self.window.height / 11)) metadata = self.file_metadata[music_path]
self.music_buttons[music_filename].button.on_click = lambda event, music_filename=music_filename: self.music_button_click(event, music_filename) self.music_buttons[music_path] = self.music_box.add(MusicItem(metadata=metadata, texture=self.thumbnails[music_path], width=self.window.width / 1.2, height=self.window.height / 11))
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].view_metadata_button.on_click = lambda event, music_path=music_path: self.open_metadata(music_filename)
self.music_box._update_size_hints() self.music_buttons["add_music"] = self.music_box.add(MusicItem(metadata=None, texture=music_icon, width=self.window.width / 1.2, height=self.window.height / 11))
self.music_buttons["add_music"] = self.music_box.add(ListItem(texture=plus_icon, font_name="Roboto", font_size=13, text="Add Music", width=self.window.width / 1.2, height=self.window.height / 11))
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()
@@ -334,6 +352,8 @@ class Main(arcade.gui.UIView):
def load_content(self): def load_content(self):
self.tab_content.clear() self.tab_content.clear()
self.playlist_content.clear() self.playlist_content.clear()
self.file_metadata.clear()
self.thumbnails.clear()
for tab in self.tab_options: for tab in self.tab_options:
expanded_tab = os.path.expanduser(tab) expanded_tab = os.path.expanduser(tab)
@@ -346,8 +366,10 @@ class Main(arcade.gui.UIView):
for filename in os.listdir(expanded_tab): for filename in os.listdir(expanded_tab):
if filename.split(".")[-1] in audio_extensions: if filename.split(".")[-1] in audio_extensions:
if f"{expanded_tab}/{filename}" not in self.thumbnails: if f"{expanded_tab}/{filename}" not in self.file_metadata:
self.thumbnails[f"{expanded_tab}/{filename}"] = get_audio_thumbnail_texture(f"{expanded_tab}/{filename}", self.window.size) sound_length, bit_rate, uploader_url, source_url, artist, title, thumbnail = extract_metadata_and_thumbnail(f"{expanded_tab}/{filename}", (int(self.window.width / 16), int(self.window.height / 9)))
self.file_metadata[f"{expanded_tab}/{filename}"] = {"sound_length": sound_length, "bit_rate": bit_rate, "uploader_url": uploader_url, "source_url": source_url, "artist": artist, "title": title}
self.thumbnails[f"{expanded_tab}/{filename}"] = thumbnail
self.tab_content[expanded_tab].append(filename) self.tab_content[expanded_tab].append(filename)
for playlist, content in self.settings_dict.get("playlists", {}).items(): for playlist, content in self.settings_dict.get("playlists", {}).items():
@@ -356,9 +378,10 @@ class Main(arcade.gui.UIView):
content.remove(file) # also removes reference from self.settings_dict["playlists"] content.remove(file) # also removes reference from self.settings_dict["playlists"]
continue continue
if file not in self.thumbnails: if file not in self.file_metadata:
self.thumbnails[file] = get_audio_thumbnail_texture(file, self.window.size) sound_length, bit_rate, uploader_url, source_url, artist, title, thumbnail = extract_metadata_and_thumbnail(file, (int(self.window.width / 16), int(self.window.height / 9)))
self.file_metadata[file] = {"sound_length": sound_length, "bit_rate": bit_rate, "uploader_url": uploader_url, "source_url": source_url, "artist": artist, "title": title}
self.thumbnails[file] = thumbnail
self.playlist_content[playlist] = content self.playlist_content[playlist] = content
def load_tabs(self): def load_tabs(self):
@@ -391,7 +414,7 @@ class Main(arcade.gui.UIView):
if len(self.queue) > 0: if len(self.queue) > 0:
music_path = self.queue.pop(0) music_path = self.queue.pop(0)
artist, title = extract_metadata(music_path) artist, title = self.file_metadata[music_path]["artist"], self.file_metadata[music_path]["title"]
music_name = f"{artist} - {title}" music_name = f"{artist} - {title}"

View File

@@ -1,13 +1,13 @@
import arcade.color import arcade.color
from arcade.types import Color from arcade.types import Color
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle from arcade.gui.widgets.buttons import UIFlatButtonStyle
from arcade.gui.widgets.slider import UISliderStyle from arcade.gui.widgets.slider import UISliderStyle
menu_background_color = (17, 17, 17) menu_background_color = (17, 17, 17)
log_dir = 'logs' log_dir = 'logs'
discord_presence_id = 1368277020332523530 discord_presence_id = 1368277020332523530
audio_extensions = ["mp3", "m4a", "mp4", "aac", "flac", "ogg", "opus", "wav"] audio_extensions = ["mp3", "m4a", "aac", "flac", "ogg", "opus", "wav"]
DARK_GRAY = Color(45, 45, 45) DARK_GRAY = Color(45, 45, 45)
GRAY = Color(70, 70, 70) GRAY = Color(70, 70, 70)

View File

@@ -1,4 +1,4 @@
import logging, sys, traceback, os, re, platform, urllib.request, io, base64, tempfile import logging, sys, traceback, os, re, platform, urllib.request, io, base64, tempfile, struct
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3 from mutagen.id3 import ID3
@@ -87,28 +87,41 @@ class UIFocusTextureButton(arcade.gui.UITextureButton):
else: else:
self.resize(width=self.width / 1.1, height=self.height / 1.1) self.resize(width=self.width / 1.1, height=self.height / 1.1)
class ListItem(arcade.gui.UIBoxLayout): class MusicItem(arcade.gui.UIBoxLayout):
def __init__(self, width: int, height: int, font_name: str, font_size: int, text: str, texture: arcade.Texture, padding=10): def __init__(self, metadata: dict, width: int, height: int, texture: arcade.Texture, padding=10):
super().__init__(width=width, height=height, space_between=padding, align="top", vertical=False) super().__init__(width=width, height=height, space_between=padding, align="top", vertical=False)
self.image = self.add(arcade.gui.UIImage( if metadata:
texture=texture, self.image = self.add(arcade.gui.UIImage(
width=width * 0.1, texture=texture,
height=height width=height * 1.5,
)) height=height,
))
self.button = self.add(arcade.gui.UITextureButton( self.button = self.add(arcade.gui.UITextureButton(
text=text, text=f"{metadata['artist']} - {metadata['title']}" if metadata else "Add Music",
texture=button_texture, texture=button_texture,
texture_hovered=button_hovered_texture, texture_hovered=button_hovered_texture,
texture_pressed=button_texture, texture_pressed=button_texture,
texture_disabled=button_texture, texture_disabled=button_texture,
style=button_style, style=button_style,
width=width * 0.9, width=width * 0.85,
height=height, height=height,
interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT] interaction_buttons=[arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT]
)) ))
if metadata:
self.view_metadata_button = self.add(arcade.gui.UITextureButton(
text="View Metadata",
texture=button_texture,
texture_hovered=button_hovered_texture,
texture_pressed=button_texture,
texture_disabled=button_texture,
style=button_style,
width=width * 0.1,
height=height,
))
def on_exception(*exc_info): def on_exception(*exc_info):
logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}") logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}")
@@ -171,88 +184,84 @@ def truncate_end(text: str, max_length: int) -> str:
return text return text
return text[:max_length - 3] + '...' return text[:max_length - 3] + '...'
def extract_metadata(filename): def extract_metadata_and_thumbnail(filename: str, thumb_resolution: tuple) -> tuple:
artist = "Unknown" artist = "Unknown"
title = "" title = ""
source_url = "Unknown"
creator_url = "Unknown"
thumb_texture = None
sound_length = 0
bit_rate = 0
basename = os.path.basename(filename) basename = os.path.basename(filename)
name_only = os.path.splitext(basename)[0] name_only = re.sub(r'\s*\[[a-zA-Z0-9\-_]{5,}\]$', '', os.path.splitext(basename)[0])
ext = os.path.splitext(filename)[1].lower().lstrip('.')
name_only = re.sub(r'\s*\[[a-zA-Z0-9\-_]{5,}\]$', '', name_only)
try: try:
thumb_audio = EasyID3(filename) thumb_audio = EasyID3(filename)
try:
artist = str(thumb_audio["artist"][0])
title = str(thumb_audio["title"][0])
except KeyError:
artist_title_match = re.search(r'^.+\s*-\s*.+$', title)
if artist_title_match:
title = title.split("- ")[1]
artist = str(thumb_audio["artist"][0]) file_audio = File(filename)
title = str(thumb_audio["title"][0]) if hasattr(file_audio, 'info'):
sound_length = round(file_audio.info.length, 2)
bit_rate = int((file_audio.info.bitrate or 0) / 1000)
artist_title_match = re.search(r'^.+\s*-\s*.+$', title) # check for Artist - Title titles, so Artist doesnt appear twice thumb_image_data = None
if ext == 'mp3':
for tag in file_audio.values():
if tag.FrameID == "APIC":
thumb_image_data = tag.data
break
elif ext in ('m4a', 'aac'):
if 'covr' in file_audio:
thumb_image_data = file_audio['covr'][0]
elif ext == 'flac':
if file_audio.pictures:
thumb_image_data = file_audio.pictures[0].data
elif ext in ('ogg', 'opus'):
if "metadata_block_picture" in file_audio:
pic_data = base64.b64decode(file_audio["metadata_block_picture"][0])
header_len = struct.unpack(">I", pic_data[0:4])[0]
thumb_image_data = pic_data[4 + header_len:]
if artist_title_match: id3 = ID3(filename)
title = title.split("- ")[1] for frame in id3.getall("WXXX"):
if frame.desc.lower() == "creator":
creator_url = frame.url
elif frame.desc.lower() == "source":
source_url = frame.url
if artist != "Unknown" and title: if thumb_image_data:
return artist, title pil_image = Image.open(io.BytesIO(thumb_image_data)).convert("RGBA")
except: pil_image = pil_image.resize(thumb_resolution)
pass thumb_texture = arcade.Texture(pil_image)
except Exception as e:
logging.debug(f"[Metadata/Thumbnail Error] {filename}: {e}")
if artist == "Unknown" or not title: if artist == "Unknown" or not title:
match = re.search(r'^(.*?)\s+-\s+(.*?)$', name_only) match = re.search(r'^(.*?)\s+-\s+(.*?)$', name_only)
if match: if match:
filename_artist, filename_title = match.groups() filename_artist, filename_title = match.groups()
if artist == "Unknown": if artist == "Unknown":
artist = filename_artist artist = filename_artist
if not title: if not title:
title = filename_title title = filename_title
return artist, title
if not title: if not title:
title = name_only title = name_only
return artist, title if thumb_texture is None:
from utils.preload import music_icon
thumb_texture = music_icon
def get_audio_thumbnail_texture(audio_path: str, window_resolution: tuple) -> arcade.Texture: return sound_length, bit_rate, creator_url, source_url, artist, title, thumb_texture
ext = os.path.splitext(audio_path)[1].lower().lstrip('.')
thumb_audio = File(audio_path)
thumb_image_data = None
try:
if ext == 'mp3':
for tag in thumb_audio.values():
if tag.FrameID == "APIC":
thumb_image_data = tag.data
break
elif ext in ('m4a', 'mp4', 'aac'):
if 'covr' in thumb_audio:
thumb_image_data = thumb_audio['covr'][0]
elif ext == 'flac':
if thumb_audio.pictures:
thumb_image_data = thumb_audio.pictures[0].data
elif ext in ('ogg', 'opus'):
if "metadata_block_picture" in thumb_audio:
pic_data = base64.b64decode(thumb_audio["metadata_block_picture"][0])
import struct
header_len = struct.unpack(">I", pic_data[0:4])[0]
thumb_image_data = pic_data[4 + header_len:]
if thumb_image_data:
pil_image = Image.open(io.BytesIO(thumb_image_data)).convert("RGBA")
pil_image = pil_image.resize((int(window_resolution[0] / 5), int(window_resolution[1] / 8)))
thumb_texture = arcade.Texture(pil_image)
return thumb_texture
except Exception as e:
logging.debug(f"[Thumbnail Error] {audio_path}: {e}")
from utils.preload import music_icon
return music_icon
def adjust_volume(input_path, volume): def adjust_volume(input_path, volume):
try: try:

6
uv.lock generated
View File

@@ -307,9 +307,9 @@ wheels = [
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.2" version = "4.14.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" } sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload_time = "2025-06-02T14:52:11.399Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" }, { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload_time = "2025-06-02T14:52:10.026Z" },
] ]