From 84e1acaab8f7e4e7e36d19e3b6847a0ab6c33759 Mon Sep 17 00:00:00 2001 From: Astounds Date: Sun, 22 Mar 2026 14:17:23 -0500 Subject: yt-dlp --- Makefile | 210 ++++++++++++++++++++++ babel.cfg | 7 + manage_translations.py | 95 ++++++++++ requirements.txt | 4 + settings.py | 20 +++ youtube/__init__.py | 24 +++ youtube/i18n_strings.py | 112 ++++++++++++ youtube/templates/base.html | 30 ++-- youtube/templates/settings.html | 14 +- youtube/watch.py | 32 ++++ youtube/ytdlp_integration.py | 78 ++++++++ youtube/ytdlp_proxy.py | 99 ++++++++++ youtube/ytdlp_service.py | 390 ++++++++++++++++++++++++++++++++++++++++ 13 files changed, 1097 insertions(+), 18 deletions(-) create mode 100644 Makefile create mode 100644 babel.cfg create mode 100644 manage_translations.py create mode 100644 youtube/i18n_strings.py create mode 100644 youtube/ytdlp_integration.py create mode 100644 youtube/ytdlp_proxy.py create mode 100644 youtube/ytdlp_service.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cf4ca99 --- /dev/null +++ b/Makefile @@ -0,0 +1,210 @@ +# yt-local Makefile +# Automated tasks for development, translations, and maintenance + +.PHONY: help install dev clean test i18n-extract i18n-init i18n-update i18n-compile i18n-stats i18n-clean setup-dev lint format backup restore + +# Variables +PYTHON := python3 +PIP := pip3 +LANG_CODE ?= es +VENV_DIR := venv +PROJECT_NAME := yt-local + +## Help +help: ## Show this help message + @echo "$(PROJECT_NAME) - Available tasks:" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' + @echo "" + @echo "Examples:" + @echo " make install # Install dependencies" + @echo " make dev # Run development server" + @echo " make i18n-extract # Extract strings for translation" + @echo " make i18n-init LANG_CODE=fr # Initialize French" + @echo " make lint # Check code style" + +## Installation and Setup +install: ## Install project dependencies + @echo "[INFO] Installing dependencies..." + $(PIP) install -r requirements.txt + @echo "[SUCCESS] Dependencies installed" + +setup-dev: ## Complete development setup + @echo "[INFO] Setting up development environment..." + $(PYTHON) -m venv $(VENV_DIR) + ./$(VENV_DIR)/bin/pip install -r requirements.txt + @echo "[SUCCESS] Virtual environment created in $(VENV_DIR)" + @echo "[INFO] Activate with: source $(VENV_DIR)/bin/activate" + +requirements: ## Update and install requirements + @echo "[INFO] Installing/updating requirements..." + $(PIP) install --upgrade pip + $(PIP) install -r requirements.txt + @echo "[SUCCESS] Requirements installed" + +## Development +dev: ## Run development server + @echo "[INFO] Starting development server..." + @echo "[INFO] Server available at: http://localhost:9010" + $(PYTHON) server.py + +run: dev ## Alias for dev + +## Testing +test: ## Run tests + @echo "[INFO] Running tests..." + @if [ -d "tests" ]; then \ + $(PYTHON) -m pytest -v; \ + else \ + echo "[WARN] No tests directory found"; \ + fi + +test-cov: ## Run tests with coverage + @echo "[INFO] Running tests with coverage..." + @if command -v pytest-cov >/dev/null 2>&1; then \ + $(PYTHON) -m pytest -v --cov=$(PROJECT_NAME) --cov-report=html; \ + else \ + echo "[WARN] pytest-cov not installed. Run: pip install pytest-cov"; \ + fi + +## Internationalization (i18n) +i18n-extract: ## Extract strings for translation + @echo "[INFO] Extracting strings for translation..." + $(PYTHON) manage_translations.py extract + @echo "[SUCCESS] Strings extracted to translations/messages.pot" + +i18n-init: ## Initialize new language (use LANG_CODE=xx) + @echo "[INFO] Initializing language: $(LANG_CODE)" + $(PYTHON) manage_translations.py init $(LANG_CODE) + @echo "[SUCCESS] Language $(LANG_CODE) initialized" + @echo "[INFO] Edit: translations/$(LANG_CODE)/LC_MESSAGES/messages.po" + +i18n-update: ## Update existing translations + @echo "[INFO] Updating existing translations..." + $(PYTHON) manage_translations.py update + @echo "[SUCCESS] Translations updated" + +i18n-compile: ## Compile translations to binary .mo files + @echo "[INFO] Compiling translations..." + $(PYTHON) manage_translations.py compile + @echo "[SUCCESS] Translations compiled" + +i18n-stats: ## Show translation statistics + @echo "[INFO] Translation statistics:" + @echo "" + @for lang_dir in translations/*/; do \ + if [ -d "$$lang_dir" ] && [ "$$lang_dir" != "translations/*/" ]; then \ + lang=$$(basename "$$lang_dir"); \ + po_file="$$lang_dir/LC_MESSAGES/messages.po"; \ + if [ -f "$$po_file" ]; then \ + total=$$(grep -c "^msgid " "$$po_file" 2>/dev/null || echo "0"); \ + translated=$$(grep -c "^msgstr \"[^\"]\+\"" "$$po_file" 2>/dev/null || echo "0"); \ + fuzzy=$$(grep -c "^#, fuzzy" "$$po_file" 2>/dev/null || echo "0"); \ + if [ "$$total" -gt 0 ]; then \ + percent=$$((translated * 100 / total)); \ + echo " [STAT] $$lang: $$translated/$$total ($$percent%) - Fuzzy: $$fuzzy"; \ + else \ + echo " [STAT] $$lang: No translations yet"; \ + fi; \ + fi \ + fi \ + done + @echo "" + +i18n-clean: ## Clean compiled translation files + @echo "[INFO] Cleaning compiled .mo files..." + find translations/ -name "*.mo" -delete + @echo "[SUCCESS] .mo files removed" + +i18n-workflow: ## Complete workflow: extract → update → compile + @echo "[INFO] Running complete translation workflow..." + @make i18n-extract + @make i18n-update + @make i18n-compile + @make i18n-stats + @echo "[SUCCESS] Translation workflow completed" + +## Code Quality +lint: ## Check code with flake8 + @echo "[INFO] Checking code style..." + @if command -v flake8 >/dev/null 2>&1; then \ + flake8 youtube/ --max-line-length=120 --ignore=E501,W503,E402 --exclude=youtube/ytdlp_service.py,youtube/ytdlp_integration.py,youtube/ytdlp_proxy.py; \ + echo "[SUCCESS] Code style check passed"; \ + else \ + echo "[WARN] flake8 not installed (pip install flake8)"; \ + fi + +format: ## Format code with black (if available) + @echo "[INFO] Formatting code..." + @if command -v black >/dev/null 2>&1; then \ + black youtube/ --line-length=120 --exclude='ytdlp_.*\.py'; \ + echo "[SUCCESS] Code formatted"; \ + else \ + echo "[WARN] black not installed (pip install black)"; \ + fi + +check-deps: ## Check installed dependencies + @echo "[INFO] Checking dependencies..." + @$(PYTHON) -c "import flask_babel; print('[OK] Flask-Babel:', flask_babel.__version__)" 2>/dev/null || echo "[ERROR] Flask-Babel not installed" + @$(PYTHON) -c "import flask; print('[OK] Flask:', flask.__version__)" 2>/dev/null || echo "[ERROR] Flask not installed" + @$(PYTHON) -c "import yt_dlp; print('[OK] yt-dlp:', yt_dlp.__version__)" 2>/dev/null || echo "[ERROR] yt-dlp not installed" + +## Maintenance +backup: ## Create translations backup + @echo "[INFO] Creating translations backup..." + @timestamp=$$(date +%Y%m%d_%H%M%S); \ + tar -czf "translations_backup_$$timestamp.tar.gz" translations/ 2>/dev/null || echo "[WARN] No translations to backup"; \ + if [ -f "translations_backup_$$timestamp.tar.gz" ]; then \ + echo "[SUCCESS] Backup created: translations_backup_$$timestamp.tar.gz"; \ + fi + +restore: ## Restore translations from backup + @echo "[INFO] Restoring translations from backup..." + @if ls translations_backup_*.tar.gz 1>/dev/null 2>&1; then \ + latest_backup=$$(ls -t translations_backup_*.tar.gz | head -1); \ + tar -xzf "$$latest_backup"; \ + echo "[SUCCESS] Restored from: $$latest_backup"; \ + else \ + echo "[ERROR] No backup files found"; \ + fi + +clean: ## Clean temporary files and caches + @echo "[INFO] Cleaning temporary files..." + find . -type f -name "*.pyc" -delete + find . -type d -name "__pycache__" -delete + find . -type f -name "*.mo" -delete + find . -type d -name ".pytest_cache" -delete + find . -type f -name ".coverage" -delete + find . -type d -name "htmlcov" -delete + @echo "[SUCCESS] Temporary files removed" + +distclean: clean ## Clean everything including venv + @echo "[INFO] Cleaning everything..." + rm -rf $(VENV_DIR) + @echo "[SUCCESS] Complete cleanup done" + +## Project Information +info: ## Show project information + @echo "[INFO] $(PROJECT_NAME) - Project information:" + @echo "" + @echo " [INFO] Directory: $$(pwd)" + @echo " [INFO] Python: $$($(PYTHON) --version)" + @echo " [INFO] Pip: $$($(PIP) --version | cut -d' ' -f1-2)" + @echo "" + @echo " [INFO] Configured languages:" + @for lang_dir in translations/*/; do \ + if [ -d "$$lang_dir" ] && [ "$$lang_dir" != "translations/*/" ]; then \ + lang=$$(basename "$$lang_dir"); \ + echo " - $$lang"; \ + fi \ + done + @echo "" + @echo " [INFO] Main files:" + @echo " - babel.cfg (i18n configuration)" + @echo " - manage_translations.py (i18n CLI)" + @echo " - youtube/i18n_strings.py (centralized strings)" + @echo " - youtube/ytdlp_service.py (yt-dlp integration)" + @echo "" + +# Default target +.DEFAULT_GOAL := help diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..5498f91 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,7 @@ +[python: youtube/**.py] +keywords = lazy_gettext:1,2 _l:1,2 +[python: server.py] +[python: settings.py] +[jinja2: youtube/templates/**.html] +extensions=jinja2.ext.i18n +encoding = utf-8 diff --git a/manage_translations.py b/manage_translations.py new file mode 100644 index 0000000..ce1e160 --- /dev/null +++ b/manage_translations.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Translation management script for yt-local + +Usage: + python manage_translations.py extract # Extract strings to messages.pot + python manage_translations.py init es # Initialize Spanish translation + python manage_translations.py update # Update all translations + python manage_translations.py compile # Compile translations to .mo files +""" +import sys +import os +import subprocess + + +def run_command(cmd): + """Run a shell command and print output""" + print(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr, file=sys.stderr) + return result.returncode + + +def extract(): + """Extract translatable strings from source code""" + print("Extracting translatable strings...") + return run_command([ + 'pybabel', 'extract', + '-F', 'babel.cfg', + '-k', 'lazy_gettext', + '-k', '_l', + '-o', 'translations/messages.pot', + '.' + ]) + + +def init(language): + """Initialize a new language translation""" + print(f"Initializing {language} translation...") + return run_command([ + 'pybabel', 'init', + '-i', 'translations/messages.pot', + '-d', 'translations', + '-l', language + ]) + + +def update(): + """Update existing translations with new strings""" + print("Updating translations...") + return run_command([ + 'pybabel', 'update', + '-i', 'translations/messages.pot', + '-d', 'translations' + ]) + + +def compile_translations(): + """Compile .po files to .mo files""" + print("Compiling translations...") + return run_command([ + 'pybabel', 'compile', + '-d', 'translations' + ]) + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + + command = sys.argv[1] + + if command == 'extract': + sys.exit(extract()) + elif command == 'init': + if len(sys.argv) < 3: + print("Error: Please specify a language code (e.g., es, fr, de)") + sys.exit(1) + sys.exit(init(sys.argv[2])) + elif command == 'update': + sys.exit(update()) + elif command == 'compile': + sys.exit(compile_translations()) + else: + print(f"Unknown command: {command}") + print(__doc__) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt index b54a1f2..eed3186 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ Flask>=1.0.3 +Flask-Babel>=4.0.0 +Babel>=2.12.0 gevent>=1.2.2 Brotli>=1.0.7 PySocks>=1.6.8 @@ -6,3 +8,5 @@ urllib3>=1.24.1 defusedxml>=0.5.0 cachetools>=4.0.0 stem>=1.8.0 +yt-dlp>=2026.01.01 +requests>=2.25.0 diff --git a/settings.py b/settings.py index 2de5efa..27cfd7d 100644 --- a/settings.py +++ b/settings.py @@ -296,6 +296,17 @@ Archive: https://archive.ph/OZQbN''', 'category': 'interface', }), + ('language', { + 'type': str, + 'default': 'en', + 'comment': 'Interface language', + 'options': [ + ('en', 'English'), + ('es', 'Español'), + ], + 'category': 'interface', + }), + ('embed_page_mode', { 'type': bool, 'label': 'Enable embed page', @@ -329,6 +340,15 @@ Archive: https://archive.ph/OZQbN''', 'hidden': True, }), + ('ytdlp_enabled', { + 'type': bool, + 'default': True, + 'comment': '''Enable yt-dlp integration for multi-language audio and subtitles''', + 'hidden': False, + 'label': 'Enable yt-dlp integration', + 'category': 'playback', + }), + ('settings_version', { 'type': int, 'default': 6, diff --git a/youtube/__init__.py b/youtube/__init__.py index 0072f74..d52ea98 100644 --- a/youtube/__init__.py +++ b/youtube/__init__.py @@ -7,12 +7,36 @@ import settings import traceback import re from sys import exc_info +from flask_babel import Babel + yt_app = flask.Flask(__name__) yt_app.config['TEMPLATES_AUTO_RELOAD'] = True yt_app.url_map.strict_slashes = False # yt_app.jinja_env.trim_blocks = True # yt_app.jinja_env.lstrip_blocks = True +# Configure Babel for i18n +import os +yt_app.config['BABEL_DEFAULT_LOCALE'] = 'en' +# Use absolute path for translations directory to avoid issues with package structure changes +_app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +yt_app.config['BABEL_TRANSLATION_DIRECTORIES'] = os.path.join(_app_root, 'translations') + +def get_locale(): + """Determine the best locale based on user preference or browser settings""" + # Check if user has a language preference in settings + if hasattr(settings, 'language') and settings.language: + locale = settings.language + print(f'[i18n] Using user preference: {locale}') + return locale + # Otherwise, use browser's Accept-Language header + # Only match languages with available translations + locale = request.accept_languages.best_match(['en', 'es']) + print(f'[i18n] Using browser language: {locale}') + return locale or 'en' + +babel = Babel(yt_app, locale_selector=get_locale) + yt_app.add_url_rule('/settings', 'settings_page', settings.settings_page, methods=['POST', 'GET']) diff --git a/youtube/i18n_strings.py b/youtube/i18n_strings.py new file mode 100644 index 0000000..47a13a3 --- /dev/null +++ b/youtube/i18n_strings.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +Centralized i18n strings for yt-local + +This file contains static strings that need to be translated but are used +dynamically in templates or generated content. By importing this module, +these strings get extracted by babel for translation. +""" + +from flask_babel import lazy_gettext as _l + +# Settings categories +CATEGORY_NETWORK = _l('Network') +CATEGORY_PLAYBACK = _l('Playback') +CATEGORY_INTERFACE = _l('Interface') + +# Common setting labels +ROUTE_TOR = _l('Route Tor') +DEFAULT_SUBTITLES_MODE = _l('Default subtitles mode') +AV1_CODEC_RANKING = _l('AV1 Codec Ranking') +VP8_VP9_CODEC_RANKING = _l('VP8/VP9 Codec Ranking') +H264_CODEC_RANKING = _l('H.264 Codec Ranking') +USE_INTEGRATED_SOURCES = _l('Use integrated sources') +ROUTE_IMAGES = _l('Route images') +ENABLE_COMMENTS_JS = _l('Enable comments.js') +ENABLE_SPONSORBLOCK = _l('Enable SponsorBlock') +ENABLE_EMBED_PAGE = _l('Enable embed page') + +# Setting names (auto-generated from setting keys) +RELATED_VIDEOS_MODE = _l('Related videos mode') +COMMENTS_MODE = _l('Comments mode') +ENABLE_COMMENT_AVATARS = _l('Enable comment avatars') +DEFAULT_COMMENT_SORTING = _l('Default comment sorting') +THEATER_MODE = _l('Theater mode') +AUTOPLAY_VIDEOS = _l('Autoplay videos') +DEFAULT_RESOLUTION = _l('Default resolution') +USE_VIDEO_PLAYER = _l('Use video player') +USE_VIDEO_DOWNLOAD = _l('Use video download') +PROXY_IMAGES = _l('Proxy images') +THEME = _l('Theme') +FONT = _l('Font') +LANGUAGE = _l('Language') +EMBED_PAGE_MODE = _l('Embed page mode') + +# Common option values +OFF = _l('Off') +ON = _l('On') +DISABLED = _l('Disabled') +ENABLED = _l('Enabled') +ALWAYS_SHOWN = _l('Always shown') +SHOWN_BY_CLICKING_BUTTON = _l('Shown by clicking button') +NATIVE = _l('Native') +NATIVE_WITH_HOTKEYS = _l('Native with hotkeys') +PLYR = _l('Plyr') + +# Theme options +LIGHT = _l('Light') +GRAY = _l('Gray') +DARK = _l('Dark') + +# Font options +BROWSER_DEFAULT = _l('Browser default') +LIBERATION_SERIF = _l('Liberation Serif') +ARIAL = _l('Arial') +VERDANA = _l('Verdana') +TAHOMA = _l('Tahoma') + +# Search and filter options +SORT_BY = _l('Sort by') +RELEVANCE = _l('Relevance') +UPLOAD_DATE = _l('Upload date') +VIEW_COUNT = _l('View count') +RATING = _l('Rating') + +# Time filters +ANY = _l('Any') +LAST_HOUR = _l('Last hour') +TODAY = _l('Today') +THIS_WEEK = _l('This week') +THIS_MONTH = _l('This month') +THIS_YEAR = _l('This year') + +# Content types +TYPE = _l('Type') +VIDEO = _l('Video') +CHANNEL = _l('Channel') +PLAYLIST = _l('Playlist') +MOVIE = _l('Movie') +SHOW = _l('Show') + +# Duration filters +DURATION = _l('Duration') +SHORT_DURATION = _l('Short (< 4 minutes)') +LONG_DURATION = _l('Long (> 20 minutes)') + +# Actions +SEARCH = _l('Search') +DOWNLOAD = _l('Download') +SUBSCRIBE = _l('Subscribe') +UNSUBSCRIBE = _l('Unsubscribe') +IMPORT = _l('Import') +EXPORT = _l('Export') +SAVE = _l('Save') +CHECK = _l('Check') +MUTE = _l('Mute') +UNMUTE = _l('Unmute') + +# Common UI elements +OPTIONS = _l('Options') +SETTINGS = _l('Settings') +ERROR = _l('Error') +LOADING = _l('loading...') diff --git a/youtube/templates/base.html b/youtube/templates/base.html index 393cc52..95207fa 100644 --- a/youtube/templates/base.html +++ b/youtube/templates/base.html @@ -35,57 +35,57 @@