diff --git a/.gitignore b/.gitignore index 08c6429f..9e4e8b19 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ dist/ songs/ qrcode.png .DS_Store -config.ini +*.ini docker-compose.yml diff --git a/pikaraoke/app.py b/pikaraoke/app.py index 69241255..48059b0e 100644 --- a/pikaraoke/app.py +++ b/pikaraoke/app.py @@ -28,21 +28,15 @@ ) from flask_babel import Babel from flask_paginate import Pagination, get_page_parameter -from selenium import webdriver -from selenium.common.exceptions import SessionNotCreatedException -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.chrome.service import Service -from selenium.webdriver.common.by import By -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait from pikaraoke import VERSION, karaoke from pikaraoke.constants import LANGUAGES from pikaraoke.lib.args import parse_pikaraoke_args from pikaraoke.lib.background_music import create_randomized_playlist +from pikaraoke.lib.ffmpeg import is_ffmpeg_installed from pikaraoke.lib.file_resolver import delete_tmp_dir, get_tmp_dir from pikaraoke.lib.get_platform import get_platform, is_raspberry_pi +from pikaraoke.lib.raspi_wifi_config import get_raspi_wifi_text from pikaraoke.lib.selenium import launch_splash_screen try: @@ -266,7 +260,7 @@ def enqueue(): d = request.form.to_dict() user = d["song-added-by"] rc = k.enqueue(song, user) - song_title = filename_from_path(song) + song_title = k.filename_from_path(song) return json.dumps({"song": song_title, "success": rc}) @@ -556,28 +550,8 @@ def splash(): ) text = "" if "Mode:Master" in status: - # Wifi is setup as a Access Point - ap_name = "" - ap_password = "" - - if os.path.isfile("/etc/raspiwifi/raspiwifi.conf"): - f = open("/etc/raspiwifi/raspiwifi.conf", "r") - - # Override the default values according to the configuration file. - for line in f.readlines(): - line = line.split("#", 1)[0] - if "ssid_prefix=" in line: - ap_name = line.split("ssid_prefix=")[1].strip() - elif "wpa_key=" in line: - ap_password = line.split("wpa_key=")[1].strip() - - if len(ap_password) > 0: - text = [ - f"Wifi Network: {ap_name} Password: {ap_password}", - f"Configure Wifi: {k.url.rpartition(':')[0]}", - ] - else: - text = [f"Wifi Network: {ap_name}", f"Configure Wifi: {k.url.rpartition(':',1)[0]}"] + # handle raspiwifi connection mode + text = get_raspi_wifi_text() else: # You are connected to Wifi as a client text = "" @@ -850,6 +824,12 @@ def main(): args = parse_pikaraoke_args() + if not is_ffmpeg_installed(): + logging.error( + "ffmpeg is not installed, which is required to run PiKaraoke. See: https://www.ffmpeg.org/" + ) + sys.exit(1) + if args.admin_password: global admin_password admin_password = args.admin_password @@ -873,7 +853,6 @@ def main(): buffer_size=args.buffer_size, hide_url=args.hide_url, hide_notifications=args.hide_notifications, - hide_raspiwifi_instructions=args.hide_raspiwifi_instructions, hide_splash_screen=args.hide_splash_screen, high_quality=args.high_quality, logo_path=args.logo_path, @@ -886,6 +865,7 @@ def main(): bg_music_path=args.bg_music_path, disable_score=args.disable_score, limit_user_songs_by=args.limit_user_songs_by, + config_file_path=args.config_file_path, ) # Expose some functions to jinja templates diff --git a/pikaraoke/karaoke.py b/pikaraoke/karaoke.py index 2c0bda13..46cba8c1 100644 --- a/pikaraoke/karaoke.py +++ b/pikaraoke/karaoke.py @@ -11,9 +11,8 @@ import time from pathlib import Path from queue import Queue -from subprocess import CalledProcessError, check_output +from subprocess import check_output from threading import Thread -from urllib.parse import urlparse import qrcode from flask_babel import _ @@ -23,17 +22,19 @@ build_ffmpeg_cmd, get_ffmpeg_version, is_transpose_enabled, + supports_hardware_h264_encoding, ) from pikaraoke.lib.file_resolver import ( FileResolver, delete_tmp_dir, is_transcoding_required, ) -from pikaraoke.lib.get_platform import ( - get_os_version, - get_platform, - is_raspberry_pi, - supports_hardware_h264_encoding, +from pikaraoke.lib.get_platform import get_os_version, get_platform, is_raspberry_pi +from pikaraoke.lib.youtube_dl import ( + build_ytdl_download_command, + get_youtube_id_from_url, + get_youtubedl_version, + upgrade_youtubedl, ) @@ -44,15 +45,7 @@ def enqueue_output(out, queue): out.close() -def decode_ignore(input): - return input.decode("utf-8", "ignore").strip() - - class Karaoke: - raspi_wifi_config_ip = "10.0.0.1" - raspi_wifi_conf_file = "/etc/raspiwifi/raspiwifi.conf" - raspi_wifi_config_installed = os.path.exists(raspi_wifi_conf_file) - queue = [] available_songs = [] @@ -78,13 +71,8 @@ class Karaoke: ffmpeg_process = None ffmpeg_log = None - ffmpeg_version = get_ffmpeg_version() - is_transpose_enabled = is_transpose_enabled() normalize_audio = False - raspberry_pi = is_raspberry_pi() - os_version = get_os_version() - config_obj = configparser.ConfigParser() def __init__( @@ -93,7 +81,6 @@ def __init__( download_path="/usr/lib/pikaraoke/songs", hide_url=False, hide_notifications=False, - hide_raspiwifi_instructions=False, hide_splash_screen=False, high_quality=False, volume=0.85, @@ -113,6 +100,7 @@ def __init__( bg_music_path=None, disable_score=False, limit_user_songs_by=0, + config_file_path="config.ini", ): logging.basicConfig( format="[%(asctime)s] %(levelname)s: %(message)s", @@ -120,13 +108,22 @@ def __init__( level=int(log_level), ) - # override with supplied constructor args if provided + # Platform-specific initializations + self.platform = get_platform() + self.os_version = get_os_version() + self.ffmpeg_version = get_ffmpeg_version() + self.is_transpose_enabled = is_transpose_enabled() + self.supports_hardware_h264_encoding = supports_hardware_h264_encoding() + self.youtubedl_version = get_youtubedl_version(youtubedl_path) + self.is_raspberry_pi = is_raspberry_pi() + + # Initialize variables + self.config_file_path = config_file_path self.port = port self.hide_url = self.get_user_preference("hide_url") or hide_url self.hide_notifications = ( self.get_user_preference("hide_notifications") or hide_notifications ) - self.hide_raspiwifi_instructions = hide_raspiwifi_instructions self.hide_splash_screen = hide_splash_screen self.download_path = download_path self.high_quality = self.get_user_preference("high_quality") or high_quality @@ -137,6 +134,7 @@ def __init__( self.get_user_preference("complete_transcode_before_play") or complete_transcode_before_play ) + self.log_level = log_level self.buffer_size = self.get_user_preference("buffer_size") or buffer_size self.youtubedl_path = youtubedl_path self.logo_path = self.default_logo_path if logo_path == None else logo_path @@ -144,7 +142,6 @@ def __init__( self.screensaver_timeout = ( self.get_user_preference("screensaver_timeout") or screensaver_timeout ) - self.url_override = url self.prefer_hostname = prefer_hostname self.disable_bg_music = self.get_user_preference("disable_bg_music") or disable_bg_music self.bg_music_volume = self.get_user_preference("bg_music_volume") or bg_music_volume @@ -153,49 +150,19 @@ def __init__( self.limit_user_songs_by = ( self.get_user_preference("limit_user_songs_by") or limit_user_songs_by ) + self.url_override = url + self.url = self.get_url() - # other initializations - self.platform = get_platform() - self.screen = None - - logging.debug( - f""" - http port: {self.port} - hide URL: {self.hide_url} - prefer hostname: {self.prefer_hostname} - url override: {self.url_override} - hide RaspiWiFi instructions: {self.hide_raspiwifi_instructions} - headless (hide splash): {self.hide_splash_screen} - splash_delay: {self.splash_delay} - screensaver_timeout: {self.screensaver_timeout} - high quality video: {self.high_quality} - download path: {self.download_path} - default volume: {self.volume} - normalize audio: {self.normalize_audio} - complete transcode before play: {self.complete_transcode_before_play} - buffer size (kb): {self.buffer_size} - youtube-dl path: {self.youtubedl_path} - logo path: {self.logo_path} - log_level: {log_level} - hide overlay: {self.hide_overlay} - disable bg music: {self.disable_bg_music} - bg music volume: {self.bg_music_volume} - bg music path: {self.bg_music_path} - disable score: {self.disable_score} - limit user songs by: {self.limit_user_songs_by} - hide notifications: {self.hide_notifications} - - platform: {self.platform} - os version: {self.os_version} - ffmpeg version: {self.ffmpeg_version} - ffmpeg transpose support: {self.is_transpose_enabled} - hardware h264 encoding: {supports_hardware_h264_encoding()} - youtubedl-version: {self.get_youtubedl_version()} -""" - ) + # Log the settings to debug level + self.log_settings_to_debug() + + # get songs from download_path + self.get_available_songs() + + self.generate_qr_code() - # Generate connection URL and QR code, - if self.raspberry_pi: + def get_url(self): + if self.is_raspberry_pi: # retry in case pi is still starting up # and doesn't have an IP yet (occurs when launched from /etc/rc.local) end_time = int(time.time()) + 30 @@ -203,7 +170,7 @@ def __init__( addresses_str = check_output(["hostname", "-I"]).strip().decode("utf-8", "ignore") addresses = addresses_str.split(" ") self.ip = addresses[0] - if not self.is_network_connected(): + if len(self.ip) < 7: logging.debug("Couldn't get IP, retrying....") else: break @@ -214,26 +181,25 @@ def __init__( if self.url_override != None: logging.debug("Overriding URL with " + self.url_override) - self.url = self.url_override + url = self.url_override else: if self.prefer_hostname: - self.url = f"http://{socket.getfqdn().lower()}:{self.port}" + url = f"http://{socket.getfqdn().lower()}:{self.port}" else: - self.url = f"http://{self.ip}:{self.port}" - self.url_parsed = urlparse(self.url) - - # get songs from download_path - self.get_available_songs() + url = f"http://{self.ip}:{self.port}" + return url - self.get_youtubedl_version() - - self.generate_qr_code() + def log_settings_to_debug(self): + output = "" + for key, value in sorted(vars(self).items()): + output += f" {key}: {value}\n" + logging.debug("\n\n" + output) # def get_user_preferences(self, preference): def get_user_preference(self, preference, default_value=False): # Try to read the config file try: - self.config_obj.read("config.ini") + self.config_obj.read(self.config_file_path) except FileNotFoundError: return default_value @@ -270,7 +236,7 @@ def change_preferences(self, preference, val): userprefs = self.config_obj["USERPREFERENCES"] userprefs[preference] = str(val) setattr(self, preference, eval(str(val))) - with open("config.ini", "w") as conf: + with open(self.config_file_path, "w") as conf: self.config_obj.write(conf) self.changed_preferences = True return [True, _("Your preferences were changed successfully")] @@ -280,7 +246,7 @@ def change_preferences(self, preference, val): def clear_preferences(self): try: - os.remove("config.ini") + os.remove(self.config_file_path) return [True, _("Your preferences were cleared successfully")] except OSError: return [False, _("Something went wrong! Your preferences were not cleared")] @@ -311,67 +277,10 @@ def get_ip(self): s.close() return IP - def get_raspi_wifi_conf_vals(self): - """Extract values from the RaspiWiFi configuration file.""" - f = open(self.raspi_wifi_conf_file, "r") - - # Define default values. - # - # References: - # - https://github.com/jasbur/RaspiWiFi/blob/master/initial_setup.py (see defaults in input prompts) - # - https://github.com/jasbur/RaspiWiFi/blob/master/libs/reset_device/static_files/raspiwifi.conf - # - server_port = "80" - ssid_prefix = "RaspiWiFi Setup" - ssl_enabled = "0" - - # Override the default values according to the configuration file. - for line in f.readlines(): - if "server_port=" in line: - server_port = line.split("t=")[1].strip() - elif "ssid_prefix=" in line: - ssid_prefix = line.split("x=")[1].strip() - elif "ssl_enabled=" in line: - ssl_enabled = line.split("d=")[1].strip() - - return (server_port, ssid_prefix, ssl_enabled) - - def get_youtubedl_version(self): - self.youtubedl_version = ( - check_output([self.youtubedl_path, "--version"]).strip().decode("utf8") - ) - return self.youtubedl_version - def upgrade_youtubedl(self): logging.info("Upgrading youtube-dl, current version: %s" % self.youtubedl_version) - try: - output = ( - check_output([self.youtubedl_path, "-U"], stderr=subprocess.STDOUT) - .decode("utf8") - .strip() - ) - except CalledProcessError as e: - output = e.output.decode("utf8") - logging.info(output) - if "You installed yt-dlp with pip or using the wheel from PyPi" in output: - # allow pip to break system packages (probably required if installed without venv) - args = ["install", "--upgrade", "yt-dlp", "--break-system-packages"] - try: - logging.info("Attempting youtube-dl upgrade via pip3...") - output = ( - check_output(["pip3"] + args, stderr=subprocess.STDOUT).decode("utf8").strip() - ) - except FileNotFoundError: - logging.info("Attempting youtube-dl upgrade via pip...") - output = ( - check_output(["pip"] + args, stderr=subprocess.STDOUT).decode("utf8").strip() - ) - self.get_youtubedl_version() - - logging.info("Done. New version: %s" % self.youtubedl_version) - - def is_network_connected(self): - return not len(self.ip) < 7 + self.youtubedl_version = upgrade_youtubedl(self.youtubedl_path) + logging.info("Done. Installed version: %s" % self.youtubedl_version) def generate_qr_code(self): logging.debug("Generating URL QR code") @@ -434,13 +343,9 @@ def download_video(self, video_url, enqueue=False, user="Pikaraoke", title=None) displayed_title = title if title else video_url # MSG: Message shown after the download is started self.log_and_send(_("Downloading video: %s" % displayed_title)) - dl_path = self.download_path + "%(title)s---%(id)s.%(ext)s" - file_quality = ( - "bestvideo[ext!=webm][height<=1080]+bestaudio[ext!=webm]/best[ext!=webm]" - if self.high_quality - else "mp4" + cmd = build_ytdl_download_command( + self.youtubedl_path, video_url, self.download_path, self.high_quality ) - cmd = [self.youtubedl_path, "-f", file_quality, "-o", dl_path, video_url] logging.debug("Youtube-dl command: " + " ".join(cmd)) rc = subprocess.call(cmd) if rc != 0: @@ -455,7 +360,7 @@ def download_video(self, video_url, enqueue=False, user="Pikaraoke", title=None) self.log_and_send(_("Downloaded: %s" % displayed_title), "success") self.get_available_songs() if enqueue: - y = self.get_youtube_id_from_url(video_url) + y = get_youtube_id_from_url(video_url) s = self.find_song_by_youtube_id(y) if s: self.enqueue(s, user, log_action=False) @@ -524,24 +429,11 @@ def find_song_by_youtube_id(self, youtube_id): logging.error("No available song found with youtube id: " + youtube_id) return None - def get_youtube_id_from_url(self, url): - if "v=" in url: # accomodates youtube.com/watch?v= and m.youtube.com/?v= - s = url.split("watch?v=") - else: # accomodates youtu.be/ - s = url.split("u.be/") - if len(s) == 2: - if "?" in s[1]: # Strip uneeded Youtube Params - s[1] = s[1][0 : s[1].index("?")] - return s[1] - else: - logging.error("Error parsing youtube id from url: " + url) - return None - def log_ffmpeg_output(self): if self.ffmpeg_log != None and self.ffmpeg_log.qsize() > 0: while self.ffmpeg_log.qsize() > 0: output = self.ffmpeg_log.get_nowait() - logging.debug("[FFMPEG] " + decode_ignore(output)) + logging.debug("[FFMPEG] " + output.decode("utf-8", "ignore").strip()) def play_file(self, file_path, semitones=0): logging.info(f"Playing file: {file_path} transposed {semitones} semitones") diff --git a/pikaraoke/lib/args.py b/pikaraoke/lib/args.py index deb15a45..06ddb0a4 100644 --- a/pikaraoke/lib/args.py +++ b/pikaraoke/lib/args.py @@ -33,6 +33,7 @@ def parse_volume(volume, type): default_prefer_hostname = False default_bg_music_volume = 0.3 default_buffer_size = 150 +default_config_file_path = "config.ini" default_dl_dir = get_default_dl_dir(platform) default_youtubedl_path = "yt-dlp" @@ -132,12 +133,6 @@ def parse_pikaraoke_args(): help="Hide notifications from the splash screen.", required=False, ) - parser.add_argument( - "--hide-raspiwifi-instructions", - action="store_true", - help="Hide RaspiWiFi setup instructions from the splash screen.", - required=False, - ) parser.add_argument( "--hide-splash-screen", "--headless", @@ -224,6 +219,12 @@ def parse_pikaraoke_args(): default="0", required=False, ), + parser.add_argument( + "--config-file-path", + help=f"Path to a config file to load settings from. Config file settings are set in the web interface or manually edited and will override command line arguments. Default {default_config_file_path}", + default=default_config_file_path, + required=False, + ), args = parser.parse_args() diff --git a/pikaraoke/lib/ffmpeg.py b/pikaraoke/lib/ffmpeg.py index b8f7a46b..397e37ed 100644 --- a/pikaraoke/lib/ffmpeg.py +++ b/pikaraoke/lib/ffmpeg.py @@ -3,8 +3,6 @@ import ffmpeg -from pikaraoke.lib.get_platform import supports_hardware_h264_encoding - def get_media_duration(file_path): try: @@ -101,9 +99,25 @@ def is_transpose_enabled(): try: filters = subprocess.run(["ffmpeg", "-filters"], capture_output=True) except FileNotFoundError: - # FFmpeg is not installed return False except IndexError: - # Unable to parse FFmpeg filters return False return "rubberband" in filters.stdout.decode() + + +def supports_hardware_h264_encoding(): + try: + codecs = subprocess.run(["ffmpeg", "-codecs"], capture_output=True) + except FileNotFoundError: + return False + except IndexError: + return False + return "h264_v4l2m2m" in codecs.stdout.decode() + + +def is_ffmpeg_installed(): + try: + subprocess.run(["ffmpeg", "-version"], capture_output=True) + except FileNotFoundError: + return False + return True diff --git a/pikaraoke/lib/file_resolver.py b/pikaraoke/lib/file_resolver.py index e28ff01d..7f65b475 100644 --- a/pikaraoke/lib/file_resolver.py +++ b/pikaraoke/lib/file_resolver.py @@ -12,7 +12,6 @@ def get_tmp_dir(): # Determine tmp directories (for things like extracted cdg files) pid = os.getpid() # for scoping tmp directories to this process - print(tempfile.gettempdir()) tmp_dir = os.path.join(tempfile.gettempdir(), f"{pid}") return tmp_dir diff --git a/pikaraoke/lib/get_platform.py b/pikaraoke/lib/get_platform.py index 4611ea85..e4611536 100644 --- a/pikaraoke/lib/get_platform.py +++ b/pikaraoke/lib/get_platform.py @@ -1,39 +1,9 @@ import io import os import platform -import re -import subprocess import sys -def get_ffmpeg_version(): - try: - # Execute the command 'ffmpeg -version' - result = subprocess.run( - ["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True - ) - # Parse the first line to get the version - first_line = result.stdout.split("\n")[0] - version_info = first_line.split(" ")[2] # Assumes the version info is the third element - return version_info - except FileNotFoundError: - return "FFmpeg is not installed" - except IndexError: - return "Unable to parse FFmpeg version" - - -def is_transpose_enabled(): - try: - filters = subprocess.run(["ffmpeg", "-filters"], capture_output=True) - except FileNotFoundError: - # FFmpeg is not installed - return False - except IndexError: - # Unable to parse FFmpeg filters - return False - return "rubberband" in filters.stdout.decode() - - def is_raspberry_pi(): try: with io.open("/sys/firmware/devicetree/base/model", "r") as m: @@ -51,12 +21,6 @@ def is_android(): def get_platform(): if sys.platform == "darwin": return "osx" - # elif sys.platform.startswith("linux"): - # for key in os.environ: - # if key == "PREFIX": - # if "termux" in os.environ[key]: - # return "Termux on Android" - # return "linux" elif is_android(): return "android" elif is_raspberry_pi(): @@ -94,22 +58,3 @@ def get_default_dl_dir(platform): def get_os_version(): return platform.version() - - -def supports_hardware_h264_encoding(): - if is_raspberry_pi(): - platform = get_platform() - - # For other platform(OrangePI etc) - if platform is None: - return False - - # Raspberry Pi >= 5 no longer has hardware GPU decoding - match = re.search(r"Raspberry Pi (\d+)", platform) - if match: - model_number = int(match.group(1)) - if model_number >= 5: - return False - return True - else: - return False diff --git a/pikaraoke/lib/raspi_wifi_config.py b/pikaraoke/lib/raspi_wifi_config.py new file mode 100644 index 00000000..fb63eaa4 --- /dev/null +++ b/pikaraoke/lib/raspi_wifi_config.py @@ -0,0 +1,54 @@ +import os + +raspi_wifi_config_ip = "10.0.0.1" +raspi_wifi_conf_file = "/etc/raspiwifi/raspiwifi.conf" +raspi_wifi_config_installed = os.path.exists(raspi_wifi_conf_file) + + +def get_raspi_wifi_conf_vals(): + """Extract values from the RaspiWiFi configuration file.""" + f = open(raspi_wifi_conf_file, "r") + + # Define default values. + # + # References: + # - https://github.com/jasbur/RaspiWiFi/blob/master/initial_setup.py (see defaults in input prompts) + # - https://github.com/jasbur/RaspiWiFi/blob/master/libs/reset_device/static_files/raspiwifi.conf + # + server_port = "80" + ssid_prefix = "RaspiWiFi Setup" + ssl_enabled = "0" + + # Override the default values according to the configuration file. + for line in f.readlines(): + if "server_port=" in line: + server_port = line.split("t=")[1].strip() + elif "ssid_prefix=" in line: + ssid_prefix = line.split("x=")[1].strip() + elif "ssl_enabled=" in line: + ssl_enabled = line.split("d=")[1].strip() + elif "wpa_key=" in line: + wpa_key = line.split("wpa_key=")[1].strip() + + return (server_port, ssid_prefix, ssl_enabled, wpa_key) + + +def get_raspi_wifi_text(url): + # Wifi is setup as a Access Point + ap_name = "" + ap_password = "" + + if os.path.isfile(raspi_wifi_conf_file): + conf_vals = get_raspi_wifi_conf_vals() + ap_name = conf_vals[1] + ap_password = conf_vals[3] + + if len(ap_password) > 0: + text = [ + f"Wifi Network: {ap_name} Password: {ap_password}", + f"Configure Wifi: {url.rpartition(':')[0]}", + ] + else: + text = [f"Wifi Network: {ap_name}", f"Configure Wifi: {url.rpartition(':',1)[0]}"] + + return text diff --git a/pikaraoke/lib/selenium.py b/pikaraoke/lib/selenium.py index e10a0576..83c7feeb 100644 --- a/pikaraoke/lib/selenium.py +++ b/pikaraoke/lib/selenium.py @@ -11,7 +11,7 @@ def launch_splash_screen(karaoke, window_size=None): - if is_raspberry_pi(): + if karaoke.is_raspberry_pi: service = service(executable_path="/usr/bin/chromedriver") else: service = None diff --git a/pikaraoke/lib/youtube_dl.py b/pikaraoke/lib/youtube_dl.py new file mode 100644 index 00000000..47f76400 --- /dev/null +++ b/pikaraoke/lib/youtube_dl.py @@ -0,0 +1,63 @@ +import logging +import subprocess + + +def get_youtubedl_version(youtubedl_path): + return subprocess.check_output([youtubedl_path, "--version"]).strip().decode("utf8") + + +def get_youtube_id_from_url(url): + if "v=" in url: # accomodates youtube.com/watch?v= and m.youtube.com/?v= + s = url.split("watch?v=") + else: # accomodates youtu.be/ + s = url.split("u.be/") + if len(s) == 2: + if "?" in s[1]: # Strip uneeded Youtube Params + s[1] = s[1][0 : s[1].index("?")] + return s[1] + else: + logging.error("Error parsing youtube id from url: " + url) + return None + + +def upgrade_youtubedl(youtubedl_path): + try: + output = ( + subprocess.check_output([youtubedl_path, "-U"], stderr=subprocess.STDOUT) + .decode("utf8") + .strip() + ) + except subprocess.CalledProcessError as e: + output = e.output.decode("utf8") + logging.info(output) + if "You installed yt-dlp with pip or using the wheel from PyPi" in output: + # allow pip to break system packages (probably required if installed without venv) + args = ["install", "--upgrade", "yt-dlp", "--break-system-packages"] + try: + logging.info("Attempting youtube-dl upgrade via pip3...") + output = ( + subprocess.check_output(["pip3"] + args, stderr=subprocess.STDOUT) + .decode("utf8") + .strip() + ) + except FileNotFoundError: + logging.info("Attempting youtube-dl upgrade via pip...") + output = ( + subprocess.check_output(["pip"] + args, stderr=subprocess.STDOUT) + .decode("utf8") + .strip() + ) + youtubedl_version = get_youtubedl_version(youtubedl_path) + + return youtubedl_version + + +def build_ytdl_download_command(youtubedl_path, video_url, download_path, high_quality=False): + dl_path = download_path + "%(title)s---%(id)s.%(ext)s" + file_quality = ( + "bestvideo[ext!=webm][height<=1080]+bestaudio[ext!=webm]/best[ext!=webm]" + if high_quality + else "mp4" + ) + cmd = [youtubedl_path, "-f", file_quality, "-o", dl_path, video_url] + return cmd