diff --git a/Dockerfile b/Dockerfile index c90cc940..6c070c15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONFAULTHANDLER=1 RUN mkdir /config /downloads && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && \ - apk add --update --no-cache bash mkvtoolnix patch aria2 coreutils curl shadow sqlite tzdata libmagic ffmpeg rtmpdump fribidi && \ + apk add --update --no-cache bash mkvtoolnix patch aria2 coreutils curl shadow sqlite tzdata libmagic ffmpeg rtmpdump fribidi git && \ useradd -u ${USER_ID:-1000} -U -d /app -s /bin/bash app && \ rm -rf /var/cache/apk/* diff --git a/FAQ.md b/FAQ.md index a605e451..5e5b4efe 100644 --- a/FAQ.md +++ b/FAQ.md @@ -39,3 +39,13 @@ whenever the link includes a playlist id. > [!NOTE] > You can also do the same via advanced options `Command options for yt-dlp` field, but presets are more convenient. + +# Install specific yt-dlp version? + +You can force specific version of `yt-dlp` by setting the `YTP_YTDLP_VERSION` environment variable for example + +```env +YTP_YTDLP_VERSION=2025.07.21 or master or nightly + +Then restart the container to apply the changes. + diff --git a/README.md b/README.md index 695bf527..17176919 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,6 @@ Certain configuration values can be set via environment variables, using the `-e | YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` | | YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` | | YTP_KEEP_ARCHIVE | Keep history of downloaded videos | `true` | -| YTP_YTDL_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` | | YTP_HOST | Which IP address to bind to | `0.0.0.0` | | YTP_PORT | Which port to bind to | `8081` | | YTP_LOG_LEVEL | Log level | `info` | @@ -312,6 +311,8 @@ Certain configuration values can be set via environment variables, using the `-e | YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` | | YTP_BROWSER_CONTROL_ENABLED | Whether to enable the file browser actions | `false` | | YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` | +| YTP_YTDLP_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` | +| YTP_YTDLP_VERSION | The version of yt-dlp to use. Defaults to latest version | `empty string` | | YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` | | YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` | | YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer | `15 */1 * * *` | diff --git a/app/library/Download.py b/app/library/Download.py index 7cdc29d6..88b2f379 100644 --- a/app/library/Download.py +++ b/app/library/Download.py @@ -111,7 +111,7 @@ class Download: self.info = info self.id = info._id self.debug = bool(config.debug) - self.debug_ytdl = bool(config.ytdl_debug) + self.debug_ytdl = bool(config.ytdlp_debug) self.cancelled = False self.tmpfilename = None self.status_queue = None diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index 16ec0d74..1ba547c9 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -676,7 +676,7 @@ class DownloadQueue(metaclass=Singleton): extract_info, config=yt_conf, url=item.url, - debug=bool(self.config.ytdl_debug), + debug=bool(self.config.ytdlp_debug), no_archive=False, follow_redirect=True, ), diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py index 65cceba2..b0ed0463 100644 --- a/app/library/PackageInstaller.py +++ b/app/library/PackageInstaller.py @@ -49,10 +49,18 @@ class PackageInstaller: sys.path.insert(0, str(self.user_site)) def action(self, pkg: str, upgrade: bool = False): - current_version = self._get_installed_version(pkg) + version: str | None = None + if "==" in pkg: + pkg, version = pkg.split("==", 1) - if upgrade and current_version: - latest_version = self._get_latest_version(pkg) + current_version: str | None = self._get_installed_version(pkg) + + if current_version and version and self.compare_versions(current_version, version): + LOG.info(f"'{pkg}' is already installed with the specified version ({version}). Skipping installation.") + return + + if upgrade and current_version and not version: + latest_version: str | None = self._get_latest_version(pkg) if latest_version and parse_version(current_version) >= parse_version(latest_version): LOG.info(f"'{pkg}' is already the latest version ({current_version}). Skipping upgrade.") return @@ -62,7 +70,7 @@ class PackageInstaller: else: LOG.info(f"'{pkg}' is not installed. Installing...") - self._install_pkg(pkg) + self._install_pkg(pkg, version=version) def _get_installed_version(self, pkg: str) -> str | None: try: @@ -82,23 +90,37 @@ class PackageInstaller: LOG.warning(f"Error while querying PyPI for '{pkg}': {e}") return None - def _install_pkg(self, pkg: str): - subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "--upgrade", - "--disable-pip-version-check", - "--target", - str(self.user_site), - "--no-warn-script-location", - pkg, - ], - check=True, - creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, - ) + def _install_pkg(self, pkg: str, version: str | None = None): + cmd: list[str] = [ + sys.executable, + "-m", + "pip", + "install", + "--no-warn-script-location", + "--upgrade", + "--target", + str(self.user_site), + ] + + if version: + if "nightly" == version and pkg == "yt_dlp": + cmd.extend(["--pre", "yt-dlp[default]"]) + elif "master" == version and pkg == "yt_dlp": + cmd.append("git+https://github.com/yt-dlp/yt-dlp.git@master") + else: + cmd.append(version if str(version).startswith("git+") else f"{pkg}=={version}") + else: + cmd.extend(["--disable-pip-version-check", pkg]) + + try: + subprocess.run( + cmd, + check=True, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + ) + except subprocess.CalledProcessError as e: + LOG.error(f"Failed to install package '{pkg}'. Error: {e}") + raise def check(self, pkgs: Packages): """ @@ -118,3 +140,28 @@ class PackageInstaller: except Exception as e: LOG.error(f"Failed to install or upgrade package '{package}'. Error message: {e!s}") LOG.exception(e) + + def compare_versions(self, current: str, target: str) -> bool: + """ + Compare versions, handling yt-dlp format where pip uses 2025.7.21 but actual is 2025.07.21 + Returns True if versions match, False otherwise + """ + if current == target: + return True + + # Handle yt-dlp version format differences + current_parts = current.split(".") + target_parts = target.split(".") + + if len(current_parts) == len(target_parts): + # Normalize parts by zero-padding single digits + current_normalized: list[str] = [ + part.zfill(2) if part.isdigit() and len(part) == 1 else part for part in current_parts + ] + target_normalized: list[str] = [ + part.zfill(2) if part.isdigit() and len(part) == 1 else part for part in target_parts + ] + if current_normalized == target_normalized: + return True + + return False diff --git a/app/library/config.py b/app/library/config.py index 1d40a753..5ca6bcad 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -46,9 +46,6 @@ class Config: keep_archive: bool = True """Keep the download archive file.""" - ytdl_debug: bool = False - """Enable yt-dlp debugging.""" - host: str = "0.0.0.0" """The host to bind the server to.""" @@ -178,6 +175,12 @@ class Config: _ytdlp_cli_mutable: str = "" """The command line options to use for yt-dlp.""" + ytdlp_version: str | None = None + """The version of yt-dlp to use, if not set, the latest version will be used.""" + + ytdlp_debug: bool = False + """Enable yt-dlp debugging.""" + is_native: bool = False "Is the application running in webview." @@ -228,7 +231,7 @@ class Config: _boolean_vars: tuple = ( "keep_archive", - "ytdl_debug", + "ytdlp_debug", "debug", "temp_keep", "access_log", @@ -531,11 +534,11 @@ class Config: if not data.get("keep_archive", False) and ytdlp_args.get("download_archive", None): data["keep_archive"] = True - data["ytdlp_version"] = Config.ytdlp_version() + data["ytdlp_version"] = Config._ytdlp_version() return data @staticmethod - def ytdlp_version(): + def _ytdlp_version(): try: from yt_dlp.version import __version__ as YTDLP_VERSION diff --git a/app/upgrader.py b/app/upgrader.py index cb353122..ec86102f 100644 --- a/app/upgrader.py +++ b/app/upgrader.py @@ -21,6 +21,8 @@ class Upgrader: config_path: Path = Path(__file__).parent.parent / "var" / "config" if env_path := os.environ.get("YTP_CONFIG_PATH", None): config_path = Path(env_path) + else: + os.environ.update({"YTP_CONFIG_PATH": str(config_path)}) if config_path.exists(): envFile: Path = config_path / ".env" @@ -33,11 +35,17 @@ class Upgrader: pkg_installer = PackageInstaller() ytdlp_auto_update: bool = os.environ.get("YTP_YTDLP_AUTO_UPDATE", "true").strip().lower() == "true" + ytdlp_version: str | None = os.environ.get("YTP_YTDLP_VERSION", "").strip() or None if ytdlp_auto_update: try: LOG.info("Checking for newer versions of 'yt-dlp' package.") - pkg_installer.action(pkg="yt_dlp", upgrade=True) + pkg_name = "yt_dlp" + if ytdlp_version: + LOG.info(f"Using specified version '{ytdlp_version}' for '{pkg_name}'.") + pkg_name += f"=={ytdlp_version}" + + pkg_installer.action(pkg=pkg_name, upgrade=True) from yt_dlp.version import __version__ as YTDLP_VERSION