8a05d5ff8f
The internal ytsage package name is kept deliberately - renaming it would touch every file and destroy the ability to merge upstream YTSage changes. - pyproject: distribution name sagetube v0.1.0, sagetube entrypoint (ytsage alias retained), URLs point at the Gitea repo with an Upstream link to YTSage. - Data dirs move to SageTube/ (fresh fork, fresh state) and the config file becomes sagetube_config.json; QApplication name and window title read SageTube. - About dialog credits Houmeres and links "Based on YTSage by oop7". - App self-update check against the upstream PyPI package is disabled by default; yt-dlp/deno/ffmpeg update flows are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
251 lines
11 KiB
Python
251 lines
11 KiB
Python
"""
|
|
This module defines centralized constants used across the YTSage application.
|
|
By storing shared values in one place, it improves consistency, readability,
|
|
and maintainability of the codebase.
|
|
|
|
Constants include:
|
|
- Asset paths for icons and notification sounds.
|
|
- OS detection and platform-specific directory paths for application data, binaries, logs, and configuration.
|
|
- Download URLs for yt-dlp and ffmpeg binaries.
|
|
- SUBPROCESS_CREATIONFLAGS: Used to specify subprocess creation flags (e.g., subprocess.CREATE_NO_WINDOW on Windows to hide the console window).
|
|
Directories are automatically created when the module is imported, ensuring the required structure exists for the application.
|
|
YTSage application constants.
|
|
|
|
"""
|
|
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Handle resource paths for both development and installed package
|
|
def get_asset_path(asset_relative_path: str) -> Path:
|
|
"""
|
|
Get the absolute path to an asset file, works both in development and installed package.
|
|
|
|
Args:
|
|
asset_relative_path: Relative path to the asset (e.g., "assets/Icon/icon.png")
|
|
|
|
Returns:
|
|
Path: Absolute path to the asset file
|
|
"""
|
|
# Check if running as a frozen executable (PyInstaller, cx_Freeze, etc.)
|
|
if getattr(sys, "frozen", False):
|
|
# Running as a frozen executable
|
|
# Try sys._MEIPASS first (PyInstaller)
|
|
if hasattr(sys, "_MEIPASS"):
|
|
asset_path = Path(sys._MEIPASS) / asset_relative_path
|
|
if asset_path.exists():
|
|
return asset_path
|
|
|
|
# For cx_Freeze, assets are typically in lib/ directory next to the executable
|
|
executable_dir = Path(sys.executable).parent
|
|
|
|
# Try with lib/ prefix (cx_Freeze standard structure)
|
|
asset_path = executable_dir / "lib" / asset_relative_path
|
|
if asset_path.exists():
|
|
return asset_path
|
|
|
|
# Try directly in executable directory
|
|
asset_path = executable_dir / asset_relative_path
|
|
if asset_path.exists():
|
|
return asset_path
|
|
|
|
# Not frozen - try importlib.resources for installed packages
|
|
try:
|
|
# Use importlib.resources (standard in Python 3.9+)
|
|
import importlib.resources as resources
|
|
try:
|
|
# Navigate to the package root and then to the asset
|
|
package_path = resources.files('ytsage')
|
|
asset_path = package_path / asset_relative_path
|
|
if asset_path.is_file():
|
|
return Path(str(asset_path))
|
|
except (ImportError, AttributeError, FileNotFoundError):
|
|
pass
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback to relative path (for development environment)
|
|
current_file = Path(__file__)
|
|
# Go up from utils to ytsage root, then to asset
|
|
ytsage_root = current_file.parent.parent.parent
|
|
asset_path = ytsage_root / asset_relative_path
|
|
|
|
return asset_path
|
|
|
|
# Assets Constants
|
|
ICON_PATH: Path = get_asset_path("assets/Icon/icon.png")
|
|
SOUND_PATH: Path = get_asset_path("assets/sound/notification.mp3")
|
|
|
|
OS_NAME: str = platform.system() # Windows ; Darwin ; Linux
|
|
|
|
IS_FROZEN = getattr(sys, "frozen", False)
|
|
USER_HOME_DIR: Path = Path.home()
|
|
|
|
# OS Specific Constants
|
|
if OS_NAME == "Windows":
|
|
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
|
|
|
|
# Always use user data directory for app data, logs, config, and binaries
|
|
# Even when frozen, we don't want to create these folders next to the executable
|
|
APP_DIR: Path = Path(os.environ.get("LOCALAPPDATA", USER_HOME_DIR / "AppData" / "Local")) / "SageTube"
|
|
APP_BIN_DIR: Path = APP_DIR / "bin"
|
|
APP_DATA_DIR: Path = APP_DIR / "data"
|
|
APP_LOG_DIR: Path = APP_DIR / "logs"
|
|
APP_CONFIG_FILE: Path = APP_DATA_DIR / "sagetube_config.json"
|
|
APP_HISTORY_FILE: Path = APP_DATA_DIR / "ytsage_history.json"
|
|
APP_THUMBNAILS_DIR: Path = APP_DATA_DIR / "thumbnails"
|
|
|
|
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"
|
|
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp.exe"
|
|
|
|
SUBPROCESS_CREATIONFLAGS: int = subprocess.CREATE_NO_WINDOW
|
|
|
|
elif OS_NAME == "Darwin": # macOS
|
|
_mac_version = platform.mac_ver()[0]
|
|
OS_FULL_NAME: str = f"macOS {_mac_version}" if _mac_version else "macOS"
|
|
|
|
# Always use user data directory for app data, logs, config, and binaries
|
|
# Even when frozen, we don't want to create these folders next to the executable
|
|
APP_DIR: Path = USER_HOME_DIR / "Library" / "Application Support" / "SageTube"
|
|
APP_BIN_DIR: Path = APP_DIR / "bin"
|
|
APP_DATA_DIR: Path = APP_DIR / "data"
|
|
APP_LOG_DIR: Path = APP_DIR / "logs"
|
|
APP_CONFIG_FILE: Path = APP_DATA_DIR / "sagetube_config.json"
|
|
APP_HISTORY_FILE: Path = APP_DATA_DIR / "ytsage_history.json"
|
|
APP_THUMBNAILS_DIR: Path = APP_DATA_DIR / "thumbnails"
|
|
|
|
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos"
|
|
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp"
|
|
|
|
SUBPROCESS_CREATIONFLAGS: int = 0
|
|
|
|
|
|
else: # Linux and other UNIX-like
|
|
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
|
|
|
|
# Always use user data directory for app data, logs, config, and binaries
|
|
# Even when frozen, we don't want to create these folders next to the executable
|
|
APP_DIR: Path = USER_HOME_DIR / ".local" / "share" / "SageTube"
|
|
APP_BIN_DIR: Path = APP_DIR / "bin"
|
|
APP_DATA_DIR: Path = APP_DIR / "data"
|
|
APP_LOG_DIR: Path = APP_DIR / "logs"
|
|
APP_CONFIG_FILE: Path = APP_DATA_DIR / "sagetube_config.json"
|
|
APP_HISTORY_FILE: Path = APP_DATA_DIR / "ytsage_history.json"
|
|
APP_THUMBNAILS_DIR: Path = APP_DATA_DIR / "thumbnails"
|
|
|
|
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
|
|
|
|
# Check for environment variable override (critical for Flatpak support)
|
|
_ytdlp_env_path = os.environ.get("YTDLP_APP_BIN_PATH")
|
|
if _ytdlp_env_path:
|
|
YTDLP_APP_BIN_PATH: Path = Path(_ytdlp_env_path)
|
|
else:
|
|
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp"
|
|
|
|
SUBPROCESS_CREATIONFLAGS: int = 0
|
|
|
|
# Documentation URLs
|
|
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
|
|
|
|
# yt-dlp SHA256 checksums URL
|
|
YTDLP_SHA256_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS"
|
|
|
|
# Deno download URLs and paths
|
|
if OS_NAME == "Windows":
|
|
DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip"
|
|
DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip.sha256sum"
|
|
DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno.exe"
|
|
elif OS_NAME == "Darwin": # macOS
|
|
DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-apple-darwin.zip"
|
|
DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-apple-darwin.zip.sha256sum"
|
|
DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno"
|
|
else: # Linux
|
|
DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip"
|
|
DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip.sha256sum"
|
|
|
|
# Check for environment variable override (critical for Flatpak support)
|
|
_deno_env_path = os.environ.get("DENO_APP_BIN_PATH")
|
|
if _deno_env_path:
|
|
DENO_APP_BIN_PATH: Path = Path(_deno_env_path)
|
|
else:
|
|
DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno"
|
|
|
|
# FFmpeg download links (Essentials build - always latest version)
|
|
FFMPEG_7Z_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z"
|
|
FFMPEG_ZIP_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
|
FFMPEG_7Z_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.sha256"
|
|
FFMPEG_ZIP_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.sha256"
|
|
FFMPEG_7Z_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.ver"
|
|
FFMPEG_ZIP_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.ver"
|
|
|
|
# =============================================================================
|
|
# File Extension Constants
|
|
# =============================================================================
|
|
# Centralized file extension definitions to avoid duplication across modules
|
|
# Use these constants for file type detection throughout the application
|
|
|
|
# Video file extensions (container formats that typically contain video)
|
|
VIDEO_EXTENSIONS: frozenset[str] = frozenset({
|
|
".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"
|
|
})
|
|
|
|
# Audio file extensions (audio-only formats)
|
|
AUDIO_EXTENSIONS: frozenset[str] = frozenset({
|
|
".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"
|
|
})
|
|
|
|
# Subtitle file extensions
|
|
SUBTITLE_EXTENSIONS: frozenset[str] = frozenset({
|
|
".vtt", ".srt", ".ass", ".ssa"
|
|
})
|
|
|
|
# Combined video and audio extensions (for file search operations)
|
|
MEDIA_EXTENSIONS: frozenset[str] = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
|
|
|
|
if __name__ == "__main__":
|
|
# If this file is run directly, print directory information; if imported, create the necessary directories for the application.
|
|
# for debug, to check os specific variable which can be different based on os.
|
|
info = {
|
|
"OS_NAME": OS_NAME,
|
|
"OS_FULL_NAME": OS_FULL_NAME,
|
|
"USER_HOME_DIR": str(USER_HOME_DIR),
|
|
"APP_DIR": str(APP_DIR),
|
|
"APP_BIN_DIR": str(APP_BIN_DIR),
|
|
"APP_DATA_DIR": str(APP_DATA_DIR),
|
|
"APP_LOG_DIR": str(APP_LOG_DIR),
|
|
"APP_CONFIG_FILE": str(APP_CONFIG_FILE),
|
|
"YTDLP_DOWNLOAD_URL": YTDLP_DOWNLOAD_URL,
|
|
"YTDLP_APP_BIN_PATH": YTDLP_APP_BIN_PATH,
|
|
"SUBPROCESS_CREATIONFLAGS": SUBPROCESS_CREATIONFLAGS,
|
|
}
|
|
for key, value in info.items():
|
|
print(f"{key}: {value}")
|
|
else:
|
|
APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
APP_BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
APP_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
APP_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Ensure custom yt-dlp directory exists if set
|
|
if OS_NAME not in ["Windows", "Darwin"]:
|
|
# Ensure parent directories exist for custom paths
|
|
if "YTDLP_APP_BIN_PATH" in globals():
|
|
YTDLP_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
if "DENO_APP_BIN_PATH" in globals():
|
|
DENO_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Put the managed-binaries dir on PATH for this process and all children:
|
|
# yt-dlp locates the Deno JS runtime (nsig/PO-token challenges) via PATH,
|
|
# and nothing else ever exports APP_BIN_DIR.
|
|
_bin_dirs = {str(APP_BIN_DIR)}
|
|
if "DENO_APP_BIN_PATH" in globals():
|
|
_bin_dirs.add(str(DENO_APP_BIN_PATH.parent))
|
|
for _bin_dir in _bin_dirs:
|
|
if _bin_dir not in os.environ.get("PATH", "").split(os.pathsep):
|
|
os.environ["PATH"] = _bin_dir + os.pathsep + os.environ.get("PATH", "")
|