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
@@ -122,9 +123,18 @@ class Downloader(arcade.gui.UIView):
title = f"{artist} - {track_title}"
try:
audio = EasyID3("downloaded_music.mp3")
audio["artist"] = artist
audio["title"] = track_title
audio = MP3("downloaded_music.mp3", ID3=ID3)
if audio.tags is None:
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()
except Exception as 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
from utils.preload import *
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 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_content = {}
self.playlist_content = {}
self.file_metadata = {}
self.thumbnails = {}
self.tab_buttons = {}
self.music_buttons = {}
@@ -257,10 +257,22 @@ class Main(arcade.gui.UIView):
self.shuffle = not self.shuffle
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):
for music_button in self.music_buttons.values():
music_button.remove(music_button.button)
music_button.remove(music_button.image)
music_button.clear()
self.music_box.remove(music_button)
del music_button
@@ -272,10 +284,13 @@ class Main(arcade.gui.UIView):
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)
self.highest_score_file = f"{self.current_tab}/{matches[0][0]}"
for match in matches:
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))
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}")
metadata = self.file_metadata[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:
self.highest_score_file = ""
@@ -283,10 +298,11 @@ class Main(arcade.gui.UIView):
self.no_music_label.visible = not 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))
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}")
metadata = self.file_metadata[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":
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)
self.highest_score_file = matches[0][0]
for match in matches:
music_filename = 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))
self.music_buttons[music_filename].button.on_click = lambda event, music_filename=music_filename: self.music_button_click(event, music_filename)
music_path = match[0]
metadata = self.file_metadata[music_path]
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:
self.highest_score_file = ""
self.no_music_label.visible = not self.playlist_content[tab]
for music_filename 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))
self.music_buttons[music_filename].button.on_click = lambda event, music_filename=music_filename: self.music_button_click(event, music_filename)
for music_path in self.playlist_content[tab]:
metadata = self.file_metadata[music_path]
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(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"] = 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"].button.on_click = lambda event: self.add_music()
self.anchor.detect_focusable_widgets()
@@ -334,6 +352,8 @@ class Main(arcade.gui.UIView):
def load_content(self):
self.tab_content.clear()
self.playlist_content.clear()
self.file_metadata.clear()
self.thumbnails.clear()
for tab in self.tab_options:
expanded_tab = os.path.expanduser(tab)
@@ -346,8 +366,10 @@ class Main(arcade.gui.UIView):
for filename in os.listdir(expanded_tab):
if filename.split(".")[-1] in audio_extensions:
if f"{expanded_tab}/{filename}" not in self.thumbnails:
self.thumbnails[f"{expanded_tab}/{filename}"] = get_audio_thumbnail_texture(f"{expanded_tab}/{filename}", self.window.size)
if f"{expanded_tab}/{filename}" not in self.file_metadata:
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)
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"]
continue
if file not in self.thumbnails:
self.thumbnails[file] = get_audio_thumbnail_texture(file, self.window.size)
if file not in self.file_metadata:
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
def load_tabs(self):
@@ -391,7 +414,7 @@ class Main(arcade.gui.UIView):
if len(self.queue) > 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}"

View File

@@ -1,13 +1,13 @@
import arcade.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
menu_background_color = (17, 17, 17)
log_dir = 'logs'
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)
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.id3 import ID3
@@ -87,28 +87,41 @@ class UIFocusTextureButton(arcade.gui.UITextureButton):
else:
self.resize(width=self.width / 1.1, height=self.height / 1.1)
class ListItem(arcade.gui.UIBoxLayout):
def __init__(self, width: int, height: int, font_name: str, font_size: int, text: str, texture: arcade.Texture, padding=10):
class MusicItem(arcade.gui.UIBoxLayout):
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)
self.image = self.add(arcade.gui.UIImage(
texture=texture,
width=width * 0.1,
height=height
))
if metadata:
self.image = self.add(arcade.gui.UIImage(
texture=texture,
width=height * 1.5,
height=height,
))
self.button = self.add(arcade.gui.UITextureButton(
text=text,
text=f"{metadata['artist']} - {metadata['title']}" if metadata else "Add Music",
texture=button_texture,
texture_hovered=button_hovered_texture,
texture_pressed=button_texture,
texture_disabled=button_texture,
style=button_style,
width=width * 0.9,
width=width * 0.85,
height=height,
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):
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[:max_length - 3] + '...'
def extract_metadata(filename):
def extract_metadata_and_thumbnail(filename: str, thumb_resolution: tuple) -> tuple:
artist = "Unknown"
title = ""
source_url = "Unknown"
creator_url = "Unknown"
thumb_texture = None
sound_length = 0
bit_rate = 0
basename = os.path.basename(filename)
name_only = os.path.splitext(basename)[0]
name_only = re.sub(r'\s*\[[a-zA-Z0-9\-_]{5,}\]$', '', name_only)
name_only = re.sub(r'\s*\[[a-zA-Z0-9\-_]{5,}\]$', '', os.path.splitext(basename)[0])
ext = os.path.splitext(filename)[1].lower().lstrip('.')
try:
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])
title = str(thumb_audio["title"][0])
file_audio = File(filename)
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:
title = title.split("- ")[1]
id3 = ID3(filename)
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:
return artist, title
except:
pass
if thumb_image_data:
pil_image = Image.open(io.BytesIO(thumb_image_data)).convert("RGBA")
pil_image = pil_image.resize(thumb_resolution)
thumb_texture = arcade.Texture(pil_image)
except Exception as e:
logging.debug(f"[Metadata/Thumbnail Error] {filename}: {e}")
if artist == "Unknown" or not title:
match = re.search(r'^(.*?)\s+-\s+(.*?)$', name_only)
if match:
filename_artist, filename_title = match.groups()
if artist == "Unknown":
artist = filename_artist
if not title:
title = filename_title
return artist, title
if not title:
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:
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
return sound_length, bit_rate, creator_url, source_url, artist, title, thumb_texture
def adjust_volume(input_path, volume):
try:

6
uv.lock generated
View File

@@ -307,9 +307,9 @@ wheels = [
[[package]]
name = "typing-extensions"
version = "4.13.2"
version = "4.14.0"
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 = [
{ 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" },
]