Files
music-player/menus/downloader.py

206 lines
8.6 KiB
Python

from mutagen.id3 import ID3, TIT2, TPE1, WXXX
from mutagen.mp3 import MP3
import arcade, arcade.gui, os, json, threading, subprocess, urllib.request, platform
from arcade.gui.experimental.focus import UIFocusGroup
from utils.music_handling import adjust_volume
from utils.constants import button_style
from utils.preload import button_texture, button_hovered_texture
class Downloader(arcade.gui.UIView):
def __init__(self, pypresence_client, *args):
super().__init__()
self.args = args
self.pypresence_client = pypresence_client
self.pypresence_client.update(state="Downloading music", start=self.pypresence_client.start_time)
with open("settings.json", "r", encoding="utf-8") as file:
self.settings_dict = json.load(file)
self.tab_options = self.settings_dict.get("tab_options", [os.path.join("~", "Music"), os.path.join("~", "Downloads")])
self.yt_dl_buffer = ""
def on_show_view(self):
super().on_show_view()
self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1)))
self.download_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="center")
self.url_name_label = self.download_box.add(arcade.gui.UILabel(text="URL or Name:", font_name="Roboto", font_size=36))
self.url_name_input = self.download_box.add(arcade.gui.UIInputText(font_name="Roboto", width=self.window.width / 2, height=self.window.height / 15, font_size=36))
self.url_name_input.activate()
self.tab_label = self.download_box.add(arcade.gui.UILabel(text="Path:", font_name="Roboto", font_size=36))
self.tab_selector = self.download_box.add(arcade.gui.UIDropdown(default=self.tab_options[0], options=self.tab_options, width=self.window.width / 2, height=self.window.height / 15, primary_style=button_style, dropdown_style=button_style, active_style=button_style))
self.status_label = self.download_box.add(arcade.gui.UILabel(text="No errors.", font_size=16, text_color=arcade.color.LIGHT_GREEN))
self.download_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Download', style=button_style, width=self.window.width / 2, height=self.window.height / 10), anchor_x="center", anchor_y="bottom", align_y=10)
self.download_button.on_click = lambda event: threading.Thread(target=self.download, daemon=True).start()
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.anchor.detect_focusable_widgets()
def on_update(self, delta_time: float) -> bool | None:
if not self.yt_dl_buffer == "download_yt_dlp":
self.status_label.text = self.yt_dl_buffer
if "WARNING" in self.yt_dl_buffer:
self.status_label.update_font(font_color=arcade.color.YELLOW)
elif "ERROR" in self.yt_dl_buffer:
self.status_label.update_font(font_color=arcade.color.RED)
else:
self.status_label.update_font(font_color=arcade.color.LIGHT_GREEN)
else:
msgbox = self.ui.add(arcade.gui.UIMessageBox(width=self.window.width / 2, height=self.window.height / 2, title="This app needs to download third-party software.", message_text="This app needs to download yt-dlp (a third-party tool) to enable video/audio downloading.\n Do you want to continue?", buttons=("Yes", "No")))
msgbox.on_action = lambda event: self.install_and_run_yt_dlp() if event.action == "Yes" else None
self.yt_dl_buffer = ''
def run_yt_dlp(self, url):
if not self.check_for_yt_dlp():
self.yt_dl_buffer = "download_yt_dlp"
return None
command = [
self.get_yt_dlp_path(), f"{url}",
"--write-info-json",
"-x", "--audio-format", "mp3",
"-o", "downloaded_music.mp3",
"--no-playlist",
"--embed-thumbnail",
"--embed-metadata"
]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
for line in process.stdout:
self.yt_dl_buffer = line.strip()
process.wait()
if process.returncode != 0:
return None
try:
with open("downloaded_music.mp3.info.json", "r", encoding="utf-8") as file:
info = json.load(file)
return info
except (json.JSONDecodeError, OSError):
self.yt_dl_buffer += "\nERROR: Failed to parse yt-dlp JSON output.\n"
return None
def download(self):
if not self.tab_selector.value:
return
url = self.url_name_input.text
if not "http" in url:
url = f"ytsearch1:{url}"
path = os.path.expanduser(self.tab_selector.value)
info = self.run_yt_dlp(url)
if not info:
return # download will get re-executed.
os.remove("downloaded_music.mp3.info.json")
os.remove("downloaded_music.info.json")
if info:
title = info.get('title', 'Unknown')
uploader = info.get('uploader', 'Unknown')
if " - " in title:
artist, track_title = title.split(" - ", 1)
else:
artist = uploader
track_title = title
title = f"{artist} - {track_title}"
try:
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}"
return
if self.settings_dict.get("normalize_audio", True):
self.yt_dl_buffer = "Normalizing audio..."
try:
adjust_volume("downloaded_music.mp3", self.settings_dict.get("normalized_volume", -8))
except Exception as e:
self.yt_dl_buffer = f"ERROR: Could not normalize volume due to an error: {e}"
return
try:
output_filename = os.path.join(path, f"{title}.mp3")
os.replace("downloaded_music.mp3", output_filename)
except Exception as e:
self.yt_dl_buffer = f"ERROR: Could not move file due to an error: {e}"
return
else:
self.yt_dl_buffer = f"ERROR: Info unavailable. This maybe due to being unable to download it due to DRM or other issues"
return
self.yt_dl_buffer = f"Successfully downloaded {title} to {path}"
def get_yt_dlp_path(self):
system = platform.system()
if system == "Windows":
return os.path.join("bin", "yt-dlp.exe")
elif system == "Darwin":
return os.path.join("bin", "yt-dlp_macos")
elif system == "Linux":
return os.path.join("bin", "yt-dlp_linux")
def check_for_yt_dlp(self):
path = self.get_yt_dlp_path()
if not os.path.exists("bin"):
os.makedirs("bin")
return os.path.exists(path)
def install_and_run_yt_dlp(self):
system = platform.system()
path = self.get_yt_dlp_path()
if system == "Windows":
url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"
elif system == "Darwin":
url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos"
elif system == "Linux":
url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux"
else:
raise RuntimeError("Unsupported OS")
urllib.request.urlretrieve(url, path)
os.chmod(path, 0o755)
threading.Thread(target=self.download, daemon=True).start()
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client, *self.args))