3 Commits

Author SHA1 Message Date
11a534ca11 Makefile: auto-bootstrap venv, fix clean targets
All checks were successful
git-sync-with-mirror / git-sync (push) Successful in 14s
CI / test (push) Successful in 45s
- Add ensure-venv target that auto-creates venv and installs deps
  on first run of dev/test/lint/i18n targets
- PYTHON/PIP now resolve to venv automatically when available
- clean: add release artefacts (yt-local/, python/, zips, get-pip.py, vc15)
- distclean: reserved for venv removal
- Fix $$ escaping for shell variables in Make recipes
- Sanitize LANG_CODE input against shell injection
- Fix test-cov (check python module, not command), info (shell vs Make expansion)
- lint/format use venv python instead of system command -v
- i18n-workflow uses native Make dependencies instead of recursive make
2026-05-03 17:29:35 -05:00
004914dccc update .gitignore
All checks were successful
git-sync-with-mirror / git-sync (push) Successful in 13s
CI / test (push) Successful in 45s
2026-05-03 17:02:29 -05:00
456283a93d fix: work around Wine build isolation failure in generate_release
All checks were successful
git-sync-with-mirror / git-sync (push) Successful in 13s
CI / test (push) Successful in 45s
On POSIX (Wine), pip's isolated build environment can't import
setuptools.build_meta. Install setuptools and wheel first, then
use --no-build-isolation for requirements.txt.

Also add python/ to .gitignore.
2026-05-03 16:53:02 -05:00
3 changed files with 154 additions and 88 deletions

12
.gitignore vendored
View File

@@ -10,6 +10,7 @@ __pycache__/
*$py.class *$py.class
*.so *.so
.Python .Python
python/
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Virtual Environments # Virtual Environments
@@ -135,6 +136,17 @@ latest-dist.zip
settings.txt settings.txt
banned_addresses.txt banned_addresses.txt
# -----------------------------------------------------------------------------
# Checksum / Hash files
# -----------------------------------------------------------------------------
*.sha512sum
*.sha256sum
*.sha1sum
*.md5sum
*.sha384sum
*.sha224sum
*.b2sum
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Temporary / Backup Files # Temporary / Backup Files
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

215
Makefile
View File

@@ -1,57 +1,88 @@
# yt-local Makefile # yt-local Makefile
# Automated tasks for development, translations, and maintenance # 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 .PHONY: help install dev clean test i18n-extract i18n-init i18n-update \
i18n-compile i18n-stats i18n-clean setup-dev lint format backup \
restore distclean info check-deps run test-cov i18n-workflow \
ensure-venv
# Variables # Variables
PYTHON := python3 SYSTEM_PYTHON := python3
PIP := pip3 LANG_CODE ?= es
LANG_CODE ?= es VENV_DIR := venv
VENV_DIR := venv PROJECT_NAME := yt-local
PROJECT_NAME := yt-local
# Use venv python/pip when the venv exists, system otherwise
VENV_PYTHON := $(VENV_DIR)/bin/python
VENV_PIP := $(VENV_DIR)/bin/pip
PYTHON = $(if $(wildcard $(VENV_PYTHON)),$(VENV_PYTHON),$(SYSTEM_PYTHON))
PIP = $(if $(wildcard $(VENV_PIP)),$(VENV_PIP),$(SYSTEM_PYTHON) -m pip)
# Patterns for release artefacts (generate_release.py)
RELEASE_DIR := yt-local
RELEASE_GLOBS := yt-local-*.zip python-dist-*.zip
RELEASE_DOWNLOADS := get-pip.py vc15_*.7z
PYTHON_DIST_DIR := python
# Validate LANG_CODE: only allow letters and underscore, 2-5 chars (e.g. es, pt_BR)
# Guards against shell injection via make i18n-init LANG_CODE="$(malicious)"
LANG_CODE_SAFE := $(shell echo '$(LANG_CODE)' | grep -xE '[a-zA-Z_]{2,5}' || echo '')
## Help -----------------------------------------------------------------------
## Help
help: ## Show this help message help: ## Show this help message
@echo "$(PROJECT_NAME) - Available tasks:" @echo "$(PROJECT_NAME) - Available tasks:"
@echo "" @echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
@echo "" @echo ""
@echo "Examples:" @echo "Examples:"
@echo " make install # Install dependencies" @echo " make install # Install dependencies"
@echo " make dev # Run development server" @echo " make dev # Run development server"
@echo " make i18n-extract # Extract strings for translation" @echo " make i18n-extract # Extract strings for translation"
@echo " make i18n-init LANG_CODE=fr # Initialize French" @echo " make i18n-init LANG_CODE=fr # Initialize French"
@echo " make lint # Check code style" @echo " make lint # Check code style"
## Installation and Setup ## Venv bootstrap (internal) --------------------------------------------------
install: ## Install project dependencies
ensure-venv:
@if [ ! -f "$(VENV_PYTHON)" ]; then \
echo "[INFO] Creating virtual environment in $(VENV_DIR)..."; \
$(SYSTEM_PYTHON) -m venv $(VENV_DIR); \
if [ -f requirements-dev.txt ]; then \
echo "[INFO] Installing dependencies (dev)..."; \
$(VENV_DIR)/bin/pip install -r requirements-dev.txt; \
else \
echo "[INFO] Installing dependencies..."; \
$(VENV_DIR)/bin/pip install -r requirements.txt; \
fi; \
echo "[SUCCESS] Virtual environment ready"; \
fi
## Installation and Setup -----------------------------------------------------
install: ensure-venv ## Install/update project dependencies
@echo "[INFO] Installing dependencies..." @echo "[INFO] Installing dependencies..."
$(PIP) install -r requirements.txt $(PIP) install -r requirements.txt
@echo "[SUCCESS] Dependencies installed" @echo "[SUCCESS] Dependencies installed"
setup-dev: ## Complete development setup setup-dev: ensure-venv ## Install dev dependencies (tests, linting)
@echo "[INFO] Setting up development environment..." @echo "[INFO] Installing dev dependencies..."
$(PYTHON) -m venv $(VENV_DIR) $(PIP) install -r requirements-dev.txt
./$(VENV_DIR)/bin/pip install -r requirements.txt @echo "[SUCCESS] Dev dependencies installed"
@echo "[SUCCESS] Virtual environment created in $(VENV_DIR)"
@echo "[INFO] Activate with: source $(VENV_DIR)/bin/activate"
requirements: ## Update and install requirements ## Development ----------------------------------------------------------------
@echo "[INFO] Installing/updating requirements..."
$(PIP) install --upgrade pip
$(PIP) install -r requirements.txt
@echo "[SUCCESS] Requirements installed"
## Development dev: ensure-venv ## Run development server
dev: ## Run development server
@echo "[INFO] Starting development server..." @echo "[INFO] Starting development server..."
@echo "[INFO] Server available at: http://localhost:9010" @echo "[INFO] Server available at: http://localhost:9010"
$(PYTHON) server.py $(PYTHON) server.py
run: dev ## Alias for dev run: dev ## Alias for dev
## Testing ## Testing --------------------------------------------------------------------
test: ## Run tests
test: ensure-venv ## Run tests
@echo "[INFO] Running tests..." @echo "[INFO] Running tests..."
@if [ -d "tests" ]; then \ @if [ -d "tests" ]; then \
$(PYTHON) -m pytest -v; \ $(PYTHON) -m pytest -v; \
@@ -59,32 +90,35 @@ test: ## Run tests
echo "[WARN] No tests directory found"; \ echo "[WARN] No tests directory found"; \
fi fi
test-cov: ## Run tests with coverage test-cov: ensure-venv ## Run tests with coverage
@echo "[INFO] Running tests with coverage..." @echo "[INFO] Running tests with coverage..."
@if command -v pytest-cov >/dev/null 2>&1; then \ @$(PYTHON) -c "import pytest_cov" 2>/dev/null && \
$(PYTHON) -m pytest -v --cov=$(PROJECT_NAME) --cov-report=html; \ $(PYTHON) -m pytest -v --cov=youtube --cov-report=html || \
else \ echo "[WARN] pytest-cov not installed. Run: make setup-dev"
echo "[WARN] pytest-cov not installed. Run: pip install pytest-cov"; \
fi
## Internationalization (i18n) ## Internationalization (i18n) ------------------------------------------------
i18n-extract: ## Extract strings for translation
i18n-extract: ensure-venv ## Extract strings for translation
@echo "[INFO] Extracting strings for translation..." @echo "[INFO] Extracting strings for translation..."
$(PYTHON) manage_translations.py extract $(PYTHON) manage_translations.py extract
@echo "[SUCCESS] Strings extracted to translations/messages.pot" @echo "[SUCCESS] Strings extracted to translations/messages.pot"
i18n-init: ## Initialize new language (use LANG_CODE=xx) i18n-init: ensure-venv ## Initialize new language (use LANG_CODE=xx)
@echo "[INFO] Initializing language: $(LANG_CODE)" @if [ -z "$(LANG_CODE_SAFE)" ]; then \
$(PYTHON) manage_translations.py init $(LANG_CODE) echo "[ERROR] Invalid LANG_CODE='$(LANG_CODE)'. Use 2-5 letters (e.g. es, pt_BR)."; \
@echo "[SUCCESS] Language $(LANG_CODE) initialized" exit 1; \
@echo "[INFO] Edit: translations/$(LANG_CODE)/LC_MESSAGES/messages.po" fi
@echo "[INFO] Initializing language: $(LANG_CODE_SAFE)"
$(PYTHON) manage_translations.py init $(LANG_CODE_SAFE)
@echo "[SUCCESS] Language $(LANG_CODE_SAFE) initialized"
@echo "[INFO] Edit: translations/$(LANG_CODE_SAFE)/LC_MESSAGES/messages.po"
i18n-update: ## Update existing translations i18n-update: ensure-venv ## Update existing translations
@echo "[INFO] Updating existing translations..." @echo "[INFO] Updating existing translations..."
$(PYTHON) manage_translations.py update $(PYTHON) manage_translations.py update
@echo "[SUCCESS] Translations updated" @echo "[SUCCESS] Translations updated"
i18n-compile: ## Compile translations to binary .mo files i18n-compile: ensure-venv ## Compile translations to binary .mo files
@echo "[INFO] Compiling translations..." @echo "[INFO] Compiling translations..."
$(PYTHON) manage_translations.py compile $(PYTHON) manage_translations.py compile
@echo "[SUCCESS] Translations compiled" @echo "[SUCCESS] Translations compiled"
@@ -98,7 +132,7 @@ i18n-stats: ## Show translation statistics
po_file="$$lang_dir/LC_MESSAGES/messages.po"; \ po_file="$$lang_dir/LC_MESSAGES/messages.po"; \
if [ -f "$$po_file" ]; then \ if [ -f "$$po_file" ]; then \
total=$$(grep -c "^msgid " "$$po_file" 2>/dev/null || echo "0"); \ total=$$(grep -c "^msgid " "$$po_file" 2>/dev/null || echo "0"); \
translated=$$(grep -c "^msgstr \"[^\"]\+\"" "$$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"); \ fuzzy=$$(grep -c "^#, fuzzy" "$$po_file" 2>/dev/null || echo "0"); \
if [ "$$total" -gt 0 ]; then \ if [ "$$total" -gt 0 ]; then \
percent=$$((translated * 100 / total)); \ percent=$$((translated * 100 / total)); \
@@ -106,50 +140,44 @@ i18n-stats: ## Show translation statistics
else \ else \
echo " [STAT] $$lang: No translations yet"; \ echo " [STAT] $$lang: No translations yet"; \
fi; \ fi; \
fi \ fi; \
fi \ fi; \
done done
@echo "" @echo ""
i18n-clean: ## Clean compiled translation files i18n-clean: ## Clean compiled translation files (.mo only)
@echo "[INFO] Cleaning compiled .mo files..." @echo "[INFO] Cleaning compiled .mo files..."
find translations/ -name "*.mo" -delete find translations/ -name "*.mo" -delete 2>/dev/null || true
@echo "[SUCCESS] .mo files removed" @echo "[SUCCESS] .mo files removed"
i18n-workflow: ## Complete workflow: extract → update → compile i18n-workflow: i18n-extract i18n-update i18n-compile i18n-stats ## 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" @echo "[SUCCESS] Translation workflow completed"
## Code Quality ## Code Quality ---------------------------------------------------------------
lint: ## Check code with flake8
lint: ensure-venv ## Check code with flake8
@echo "[INFO] Checking code style..." @echo "[INFO] Checking code style..."
@if command -v flake8 >/dev/null 2>&1; then \ @$(PYTHON) -c "import flake8" 2>/dev/null && \
flake8 youtube/ --max-line-length=120 --ignore=E501,W503,E402 --exclude=youtube/ytdlp_service.py,youtube/ytdlp_integration.py,youtube/ytdlp_proxy.py; \ $(PYTHON) -m flake8 youtube/ --max-line-length=120 --ignore=E501,W503,E402 \
echo "[SUCCESS] Code style check passed"; \ --exclude=youtube/ytdlp_service.py,youtube/ytdlp_integration.py,youtube/ytdlp_proxy.py && \
else \ echo "[SUCCESS] Code style check passed" || \
echo "[WARN] flake8 not installed (pip install flake8)"; \ echo "[WARN] flake8 not installed. Run: make setup-dev"
fi
format: ## Format code with black (if available) format: ensure-venv ## Format code with black (if available)
@echo "[INFO] Formatting code..." @echo "[INFO] Formatting code..."
@if command -v black >/dev/null 2>&1; then \ @$(PYTHON) -c "import black" 2>/dev/null && \
black youtube/ --line-length=120 --exclude='ytdlp_.*\.py'; \ $(PYTHON) -m black youtube/ --line-length=120 --exclude='ytdlp_.*\.py' && \
echo "[SUCCESS] Code formatted"; \ echo "[SUCCESS] Code formatted" || \
else \ echo "[WARN] black not installed. Run: make setup-dev"
echo "[WARN] black not installed (pip install black)"; \
fi
check-deps: ## Check installed dependencies check-deps: ensure-venv ## Check installed dependencies
@echo "[INFO] Checking 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_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 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" @$(PYTHON) -c "import yt_dlp; print('[OK] yt-dlp:', yt_dlp.__version__)" 2>/dev/null || echo "[ERROR] yt-dlp not installed"
## Maintenance ## Maintenance ----------------------------------------------------------------
backup: ## Create translations backup backup: ## Create translations backup
@echo "[INFO] Creating translations backup..." @echo "[INFO] Creating translations backup..."
@timestamp=$$(date +%Y%m%d_%H%M%S); \ @timestamp=$$(date +%Y%m%d_%H%M%S); \
@@ -158,24 +186,39 @@ backup: ## Create translations backup
echo "[SUCCESS] Backup created: translations_backup_$$timestamp.tar.gz"; \ echo "[SUCCESS] Backup created: translations_backup_$$timestamp.tar.gz"; \
fi fi
restore: ## Restore translations from backup restore: ## Restore translations from latest backup
@echo "[INFO] Restoring translations from backup..." @echo "[INFO] Restoring translations from backup..."
@if ls translations_backup_*.tar.gz 1>/dev/null 2>&1; then \ @latest_backup=$$(find . -maxdepth 1 -name 'translations_backup_*.tar.gz' -print0 | \
latest_backup=$$(ls -t translations_backup_*.tar.gz | head -1); \ xargs -0 ls -t 2>/dev/null | head -1); \
if [ -n "$$latest_backup" ]; then \
tar -xzf "$$latest_backup"; \ tar -xzf "$$latest_backup"; \
echo "[SUCCESS] Restored from: $$latest_backup"; \ echo "[SUCCESS] Restored from: $$latest_backup"; \
else \ else \
echo "[ERROR] No backup files found"; \ echo "[ERROR] No backup files found"; \
fi fi
clean: ## Clean temporary files and caches ## Cleanup --------------------------------------------------------------------
clean: ## Clean temporary files, caches, and release artefacts
@echo "[INFO] Cleaning temporary files..." @echo "[INFO] Cleaning temporary files..."
# Python byte-code and caches
find . -type f -name "*.pyc" -delete find . -type f -name "*.pyc" -delete
find . -type d -name "__pycache__" -delete find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
# Compiled translation files (project-wide)
find . -type f -name "*.mo" -delete find . -type f -name "*.mo" -delete
find . -type d -name ".pytest_cache" -delete # Pytest cache and coverage data
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name ".coverage" -delete find . -type f -name ".coverage" -delete
find . -type d -name "htmlcov" -delete # Misc temporary / backup artefacts
find . -type f \( -name "*.tmp" -o -name "*.bak" -o -name "*.log" \) -delete
find . -type d -name "*.cache" -exec rm -rf {} + 2>/dev/null || true
# Checksum files
find . -type f \( -name "*.sha512sum" -o -name "*.sha256sum" -o -name "*.sha1sum" \
-o -name "*.md5sum" -o -name "*.b2sum" \) -delete
# Release artefacts (generate_release.py)
rm -rf $(RELEASE_DIR)/ $(PYTHON_DIST_DIR)/
rm -f $(RELEASE_GLOBS) $(RELEASE_DOWNLOADS)
@echo "[SUCCESS] Temporary files removed" @echo "[SUCCESS] Temporary files removed"
distclean: clean ## Clean everything including venv distclean: clean ## Clean everything including venv
@@ -183,20 +226,20 @@ distclean: clean ## Clean everything including venv
rm -rf $(VENV_DIR) rm -rf $(VENV_DIR)
@echo "[SUCCESS] Complete cleanup done" @echo "[SUCCESS] Complete cleanup done"
## Project Information ## Project Information --------------------------------------------------------
info: ## Show project information
info: ensure-venv ## Show project information
@echo "[INFO] $(PROJECT_NAME) - Project information:" @echo "[INFO] $(PROJECT_NAME) - Project information:"
@echo "" @echo ""
@echo " [INFO] Directory: $$(pwd)" @echo " [INFO] Directory: $$(pwd)"
@echo " [INFO] Python: $$($(PYTHON) --version)" @echo " [INFO] Python: $$($(PYTHON) --version 2>&1)"
@echo " [INFO] Pip: $$($(PIP) --version | cut -d' ' -f1-2)" @echo " [INFO] Pip: $$($(PIP) --version 2>&1 | cut -d' ' -f1-2)"
@echo "" @echo ""
@echo " [INFO] Configured languages:" @echo " [INFO] Configured languages:"
@for lang_dir in translations/*/; do \ @for lang_dir in translations/*/; do \
if [ -d "$$lang_dir" ] && [ "$$lang_dir" != "translations/*/" ]; then \ if [ -d "$$lang_dir" ] && [ "$$lang_dir" != "translations/*/" ]; then \
lang=$$(basename "$$lang_dir"); \ echo " - $$(basename $$lang_dir)"; \
echo " - $$lang"; \ fi; \
fi \
done done
@echo "" @echo ""
@echo " [INFO] Main files:" @echo " [INFO] Main files:"

View File

@@ -222,7 +222,14 @@ log('Inserting Microsoft C Runtime')
check_subp(subprocess.run([r'7z', '-y', 'e', '-opython', visual_c_name, visual_c_path_to_dlls])) check_subp(subprocess.run([r'7z', '-y', 'e', '-opython', visual_c_name, visual_c_path_to_dlls]))
log('Installing dependencies') log('Installing dependencies')
wine_run(['./python/python.exe', '-I', '-m', 'pip', 'install', '--no-compile', '-r', './requirements.txt']) pip_install = ['./python/python.exe', '-I', '-m', 'pip', 'install', '--no-compile']
if os.name == 'posix':
# Wine's isolated build environment can't import setuptools.build_meta,
# so we install build tools first and disable isolation.
wine_run(pip_install + ['setuptools', 'wheel'])
wine_run(pip_install + ['--no-build-isolation', '-r', './requirements.txt'])
else:
wine_run(pip_install + ['-r', './requirements.txt'])
log('Uninstalling unnecessary gevent stuff') log('Uninstalling unnecessary gevent stuff')
wine_run(['./python/python.exe', '-I', '-m', 'pip', 'uninstall', '--yes', 'cffi', 'pycparser']) wine_run(['./python/python.exe', '-I', '-m', 'pip', 'uninstall', '--yes', 'cffi', 'pycparser'])
@@ -231,7 +238,11 @@ shutil.rmtree(r'./python/Lib/site-packages/gevent/testing')
remove_files_with_extensions(r'./python/Lib/site-packages/gevent', ['.html']) # bloated html documentation remove_files_with_extensions(r'./python/Lib/site-packages/gevent', ['.html']) # bloated html documentation
log('Uninstalling pip and others') log('Uninstalling pip and others')
wine_run(['./python/python.exe', '-I', '-m', 'pip', 'uninstall', '--yes', 'pip', 'wheel']) pip_uninstall = ['./python/python.exe', '-I', '-m', 'pip', 'uninstall', '--yes']
if os.name == 'posix':
wine_run(pip_uninstall + ['pip', 'setuptools', 'wheel'])
else:
wine_run(pip_uninstall + ['pip', 'wheel'])
log('Removing pyc files') # Have to do this because get-pip and some packages don't respect --no-compile log('Removing pyc files') # Have to do this because get-pip and some packages don't respect --no-compile
remove_files_with_extensions(r'./python', ['.pyc']) remove_files_with_extensions(r'./python', ['.pyc'])