Merge branch 'beta'

This commit is contained in:
oop7
2026-03-07 20:56:53 +02:00
76 changed files with 7014 additions and 2886 deletions
+13 -4
View File
@@ -8,8 +8,8 @@ This repository uses GitHub Actions to automatically build and release YTSage fo
The workflows are triggered manually via the GitHub Actions "Workflow Dispatch" interface. This allows you to specify the version number explicitly (e.g., `1.0.0`) at runtime. The workflows are triggered manually via the GitHub Actions "Workflow Dispatch" interface. This allows you to specify the version number explicitly (e.g., `1.0.0`) at runtime.
### Workflows ### Workflows
- **Create All Releases** (`release-all.yml`): The master workflow. Triggering this will automatically run the Windows, Linux, and macOS builds in parallel with the version you provide. - **Create All Releases** (`release-all.yml`): The master workflow. Triggering this will automatically run the Windows, Linux, macOS, and PyPI builds in parallel with the version you provide.
- **Platform Specific**: You can also trigger `Build Windows Release`, `Build Linux Release`, or `Build macOS Release` individually if you only need updates for one OS. - **Platform Specific**: You can also trigger `Build Windows Release`, `Build Linux Release`, `Build macOS Release`, or `Build PyPI Package` individually.
### Build Process ### Build Process
1. **Setup**: Uses Python 3.13 on all platforms 1. **Setup**: Uses Python 3.13 on all platforms
@@ -43,18 +43,26 @@ The workflow creates the following files based on the platform:
#### Windows #### Windows
- `YTSage-v{version}-portable.zip` - Standard portable version - `YTSage-v{version}-portable.zip` - Standard portable version
- `YTSage-v{version}-ffmpeg-portable.zip` - FFmpeg bundle portable - `YTSage-v{version}-ffmpeg-portable.zip` - FFmpeg bundle portable
- `YTSage-v{version}-Setup.exe` - Standard installer
- `YTSage-v{version}-ffmpeg-Setup.exe` - FFmpeg bundle installer
#### Linux #### Linux
- `YTSage-v{version}-{arch}.AppImage` - AppImage portable (x86_64, aarch64) - `YTSage-v{version}-{arch}.AppImage` - AppImage portable (x86_64, aarch64)
- `YTSage-v{version}-{arch}.rpm` - RPM package - `YTSage-v{version}-{arch}.rpm` - RPM package
- `YTSage-v{version}-{arch}.deb` - Debian package - `YTSage-v{version}-{arch}.deb` - Debian package
- `YTSage-v{version}-{arch}.flatpak` - Flatpak bundle
#### macOS #### macOS
- `YTSage-v{version}-{arch}.app.zip` - Zipped application bundle (x64, arm64) - `YTSage-v{version}-arm64.app.zip` - Zipped application bundle
- `YTSage-v{version}-{arch}.dmg` - Disk image installer - `YTSage-v{version}-arm64.dmg` - Disk image installer
#### PyPI
- `ytsage-{version}-py3-none-any.whl` - Python Wheel
- `ytsage-{version}.tar.gz` - Source Distribution
## Workflow Features ## Workflow Features
- **PyPI**: Standard Python build system (Wheel & Source)
### Multi-Platform Support ### Multi-Platform Support
- **Windows**: Uses PowerShell scripts with cx_Freeze - **Windows**: Uses PowerShell scripts with cx_Freeze
- **Linux**: Uses Bash scripts with cx_Freeze, creates AppImage, RPM, and DEB - **Linux**: Uses Bash scripts with cx_Freeze, creates AppImage, RPM, and DEB
@@ -90,6 +98,7 @@ The workflow files are located in `.github/workflows/`:
- `release-all.yml` - Master workflow that orchestrates the others - `release-all.yml` - Master workflow that orchestrates the others
- `build-windows.yml` - Windows builds logic - `build-windows.yml` - Windows builds logic
- `build-linux.yml` - Linux builds logic - `build-linux.yml` - Linux builds logic
- `build-pypi.yml` - PyPI build logic
- `build-macos.yml` - macOS builds logic - `build-macos.yml` - macOS builds logic
### Key Configuration Options ### Key Configuration Options
+4 -8
View File
@@ -45,14 +45,10 @@ assignees: ''
Attach screenshots (for GUI issues) or terminal logs (for CLI errors). Attach screenshots (for GUI issues) or terminal logs (for CLI errors).
To collect log files: To collect log files:
1. Go to the logs folder: 1. Open YTSage and reproduce the issue.
- Windows: %LOCALAPPDATA%\YTSage\logs 2. Click the **About** button.
- macOS: ~/Library/Application Support/YTSage/logs 3. Click **Logs** (📂) to open the logs folder.
- Linux: ~/.local/share/YTSage/logs 4. Attach `ytsage.log` and `ytsage_error.log`.
2. Delete all files in that folder
3. Open the app and reproduce the issue
4. Go back to the logs folder - you should find two new log files (ytsage.log, ytsage_errors.log)
5. Attach those log files to this issue
Use ``` to format logs: Use ``` to format logs:
--> -->
+126 -25
View File
@@ -49,7 +49,7 @@ jobs:
path: | path: |
venv venv
~/.cache/pip ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
@@ -67,7 +67,7 @@ jobs:
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --no-cache-dir -r requirements.txt pip install --no-cache-dir .
pip install --no-cache-dir cx_Freeze pip install --no-cache-dir cx_Freeze
- name: Prepare build variables - name: Prepare build variables
@@ -82,6 +82,13 @@ jobs:
- name: Create cx_Freeze setup script (Linux) - name: Create cx_Freeze setup script (Linux)
shell: bash shell: bash
run: | run: |
# Create entry point script
cat > ytsage_entry.py <<'ENTRY'
from ytsage.main import main
if __name__ == "__main__":
main()
ENTRY
cat > setup_cxfreeze.py <<'PY' cat > setup_cxfreeze.py <<'PY'
import os import os
import sys import sys
@@ -92,40 +99,48 @@ jobs:
# Prepare include_files list # Prepare include_files list
include_files_list = [ include_files_list = [
("src", "src"), ("ytsage/assets/Icon", "lib/assets/Icon"),
("assets/branding/icons", "lib/assets/branding/icons"), ("ytsage/assets/sound", "lib/assets/sound"),
("assets/Icon", "lib/assets/Icon"), ("ytsage/languages", "lib/languages"),
("assets/sound", "lib/assets/sound"), ("branding/icons", "lib/assets/branding/icons"),
("languages", "lib/languages"),
("ytsage.desktop", "share/applications/ytsage.desktop"), ("ytsage.desktop", "share/applications/ytsage.desktop"),
("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"), ("branding/icons/icon.png", "share/pixmaps/ytsage.png"),
] ]
build_exe_options = dict( build_exe_options = dict(
optimize=2, optimize=2,
silent=True,
packages=[ packages=[
"ytsage",
"PySide6.QtCore", "PySide6.QtCore",
"PySide6.QtGui", "PySide6.QtGui",
"PySide6.QtWidgets", "PySide6.QtWidgets",
"PySide6.QtMultimedia",
"PySide6.QtNetwork",
"PySide6.QtDBus",
"PySide6.QtSvg",
"requests", "requests",
"PIL", "PIL",
"packaging", "packaging",
"markdown", "markdown",
"pyglet",
"loguru", "loguru",
"setuptools", "setuptools",
], ],
zip_include_packages=[
"PySide6",
"shiboken6",
"requests",
"PIL",
"packaging",
],
excludes=[ excludes=[
"PySide6.QtBluetooth", "PySide6.QtBluetooth",
"PySide6.QtNetwork",
"PySide6.QtOpenGL", "PySide6.QtOpenGL",
"PySide6.QtPrintSupport", "PySide6.QtPrintSupport",
"PySide6.QtSvg",
"PySide6.QtTest", "PySide6.QtTest",
"PySide6.QtXml", "PySide6.QtXml",
"PySide6.QtSql", "PySide6.QtSql",
"PySide6.QtHelp", "PySide6.QtHelp",
"PySide6.QtMultimedia",
"PySide6.QtQml", "PySide6.QtQml",
"PySide6.QtQuick", "PySide6.QtQuick",
"PySide6.QtWebEngineCore", "PySide6.QtWebEngineCore",
@@ -140,6 +155,9 @@ jobs:
"unittest", "unittest",
"test", "test",
"tests", "tests",
"pydoc",
"doctest",
"email",
], ],
include_files=include_files_list, include_files=include_files_list,
# Bundle all dependencies - avoid system library references # Bundle all dependencies - avoid system library references
@@ -151,9 +169,9 @@ jobs:
executables = [ executables = [
Executable( Executable(
script="main.py", script="ytsage_entry.py",
target_name="ytsage", target_name="ytsage",
icon="assets/branding/icons/icon.png", icon="branding/icons/icon.png",
) )
] ]
@@ -194,18 +212,101 @@ jobs:
set -e set -e
source venv/bin/activate source venv/bin/activate
# Ensure a clean build and build_exe first # Ensure a clean build and build_exe first with -OO
python setup_cxfreeze.py build_exe python -OO setup_cxfreeze.py build_exe
# Remove screenshots to reduce size before packaging # Clean unnecessary files from build directory before packaging
build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true) build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true)
if [ -n "$build_dir" ] && [ -d "$build_dir/lib/assets/branding/screenshots" ]; then
rm -rf "$build_dir/lib/assets/branding/screenshots" if [ -n "$build_dir" ]; then
echo "Removed screenshots folder from $build_dir" echo "Trimming files in $build_dir..."
lib_dir="$build_dir/lib"
if [ -d "$lib_dir" ]; then
# 1. Remove screenshots
if [ -d "$lib_dir/assets/branding/screenshots" ]; then
rm -rf "$lib_dir/assets/branding/screenshots"
echo "Removed screenshots folder"
fi fi
# Build AppImage # 2. Remove unused Qt translations
python setup_cxfreeze.py bdist_appimage if [ -d "$lib_dir/PySide6/translations" ]; then
rm -rf "$lib_dir/PySide6/translations"
echo "Removed Qt translations"
fi
# 3. Remove unused Qt plugins
plugins_dir="$lib_dir/PySide6/plugins"
if [ -d "$plugins_dir" ]; then
for plugin in designer pdf sql help qml quick webengine bluetooth opengl printsupport test xml; do
if [ -d "$plugins_dir/$plugin" ]; then
rm -rf "$plugins_dir/$plugin"
echo "Removed plugin: $plugin"
fi
done
# Specific cleanups (imageformats)
rm -f "$plugins_dir/imageformats/libqpdf.so"
fi
# 4. Remove bloat shared libraries (Recursively finds in lib/ and lib/PySide6/)
# Matches libQt6Qml.so.6, Qt6Qml.abi3.so, etc.
echo "Removing bloat Qt libraries..."
find "$lib_dir" -name "*Qt6Web*" -delete
find "$lib_dir" -name "*Qt6Pdf*" -delete
find "$lib_dir" -name "*Qt6Qml*" -delete
find "$lib_dir" -name "*Qt6Quick*" -delete
find "$lib_dir" -name "*Qt6VirtualKeyboard*" -delete
find "$lib_dir" -name "*Qt6OpenGL*" -delete
# 5. Remove build tools and unused python modules
for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do
rm -rf "$lib_dir/$tool"
echo "Removed build tool/module: $tool"
done
else
echo "Warning: lib directory not found inside build_dir"
fi
else
echo "Warning: Build directory not found, cannot trim files"
fi
# Build AppImage manually to avoid cx_Freeze rebuilding (and untrimming) the artifacts
echo "Creating AppImage manually..."
mkdir -p dist
# Download appimagetool
wget -q -O appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool
# Setup AppDir structure (Root layout matches cx_Freeze build output exactly)
rm -rf AppDir
mkdir -p AppDir
# Copy trimmed build content DIRECTLY to AppDir root
# This keeps 'lib' adjacent to 'ytsage', preventing 'ModuleNotFoundError: encodings'
cp -r "$build_dir"/* AppDir/
# Setup metadata
cp ytsage.desktop AppDir/
# Fix Exec path for AppImage context (binary is in root, not /usr/bin)
sed -i 's|Exec=/usr/bin/ytsage|Exec=ytsage|g' AppDir/ytsage.desktop
cp branding/icons/icon.png AppDir/ytsage.png
# .DirIcon for file managers
cp branding/icons/icon.png AppDir/.DirIcon
# Create AppRun as a script to correctly set environment
cat > AppDir/AppRun <<'APPRUN'
#!/bin/sh
SELF=$(readlink -f "$0")
HERE=$(dirname "$SELF")
export LD_LIBRARY_PATH="$HERE/lib:$LD_LIBRARY_PATH"
exec "$HERE/ytsage" "$@"
APPRUN
chmod +x AppDir/AppRun
# Build AppImage (using --appimage-extract-and-run to avoid FUSE issues in CI)
# We explicitly set ARCH for appimagetool
ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "dist/ytsage-v${version}.AppImage"
# Build RPM manually with proper spec file # Build RPM manually with proper spec file
version="${{ steps.get_version.outputs.VERSION }}" version="${{ steps.get_version.outputs.VERSION }}"
@@ -257,7 +358,7 @@ jobs:
# Install desktop file and icon # Install desktop file and icon
install -m 0644 ${workspace_root}/ytsage.desktop %{buildroot}/usr/share/applications/ytsage.desktop install -m 0644 ${workspace_root}/ytsage.desktop %{buildroot}/usr/share/applications/ytsage.desktop
install -m 0644 ${workspace_root}/assets/branding/icons/icon.png %{buildroot}/usr/share/pixmaps/ytsage.png install -m 0644 ${workspace_root}/branding/icons/icon.png %{buildroot}/usr/share/pixmaps/ytsage.png
%files %files
/opt/ytsage /opt/ytsage
@@ -337,7 +438,7 @@ jobs:
# Desktop file and icon # Desktop file and icon
install -m 0644 ytsage.desktop "$pkgroot/usr/share/applications/ytsage.desktop" install -m 0644 ytsage.desktop "$pkgroot/usr/share/applications/ytsage.desktop"
install -m 0644 assets/branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png" install -m 0644 branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png"
# Control file # Control file
cat > "$pkgroot/DEBIAN/control" <<CONTROL cat > "$pkgroot/DEBIAN/control" <<CONTROL
@@ -435,7 +536,7 @@ jobs:
"cp -r dist/ytsage-v${version}-*/* /app/share/ytsage/", "cp -r dist/ytsage-v${version}-*/* /app/share/ytsage/",
"ln -s /app/share/ytsage/ytsage /app/bin/ytsage", "ln -s /app/share/ytsage/ytsage /app/bin/ytsage",
"install -D flatpak.desktop /app/share/applications/$APP_ID.desktop", "install -D flatpak.desktop /app/share/applications/$APP_ID.desktop",
"install -D assets/branding/icons/icon.png /app/share/icons/hicolor/128x128/apps/$APP_ID.png" "install -D branding/icons/icon.png /app/share/icons/hicolor/128x128/apps/$APP_ID.png"
], ],
"sources": [ "sources": [
{ {
+112 -54
View File
@@ -50,7 +50,7 @@ jobs:
venv venv
~/.cache/pip ~/.cache/pip
~/Library/Caches/pip ~/Library/Caches/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
@@ -60,7 +60,7 @@ jobs:
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --no-cache-dir -r requirements.txt pip install --no-cache-dir .
pip install --no-cache-dir cx_Freeze dmgbuild pip install --no-cache-dir cx_Freeze dmgbuild
- name: Prepare build variables - name: Prepare build variables
@@ -80,6 +80,13 @@ jobs:
- name: Create cx_Freeze setup script - name: Create cx_Freeze setup script
shell: bash shell: bash
run: | run: |
# Create entry point script
cat > ytsage_entry.py <<'ENTRY'
from ytsage.main import main
if __name__ == "__main__":
main()
ENTRY
cat > setup_cxfreeze.py <<'PY' cat > setup_cxfreeze.py <<'PY'
import os import os
from cx_Freeze import setup, Executable from cx_Freeze import setup, Executable
@@ -88,21 +95,31 @@ jobs:
build_exe_options = dict( build_exe_options = dict(
optimize=2, optimize=2,
silent=True,
packages=[ packages=[
"ytsage",
"PySide6.QtCore", "PySide6.QtCore",
"PySide6.QtGui", "PySide6.QtGui",
"PySide6.QtWidgets", "PySide6.QtWidgets",
"PySide6.QtMultimedia",
"PySide6.QtNetwork",
"PySide6.QtDBus",
"requests", "requests",
"PIL", "PIL",
"packaging", "packaging",
"markdown", "markdown",
"pyglet",
"loguru", "loguru",
"setuptools", "setuptools",
], ],
zip_include_packages=[
"PySide6",
"shiboken6",
"requests",
"PIL",
"packaging",
],
excludes=[ excludes=[
"PySide6.QtBluetooth", "PySide6.QtBluetooth",
"PySide6.QtNetwork",
"PySide6.QtOpenGL", "PySide6.QtOpenGL",
"PySide6.QtPrintSupport", "PySide6.QtPrintSupport",
"PySide6.QtSvg", "PySide6.QtSvg",
@@ -110,7 +127,6 @@ jobs:
"PySide6.QtXml", "PySide6.QtXml",
"PySide6.QtSql", "PySide6.QtSql",
"PySide6.QtHelp", "PySide6.QtHelp",
"PySide6.QtMultimedia",
"PySide6.QtQml", "PySide6.QtQml",
"PySide6.QtQuick", "PySide6.QtQuick",
"PySide6.QtWebEngineCore", "PySide6.QtWebEngineCore",
@@ -125,21 +141,23 @@ jobs:
"unittest", "unittest",
"test", "test",
"tests", "tests",
"pydoc",
"doctest",
"email",
], ],
include_files=[ include_files=[
("src", "src"), ("ytsage/assets/Icon", "lib/assets/Icon"),
("assets/branding/icons", "lib/assets/branding/icons"), ("ytsage/assets/sound", "lib/assets/sound"),
("assets/Icon", "lib/assets/Icon"), ("ytsage/languages", "lib/languages"),
("assets/sound", "lib/assets/sound"), ("branding/icons", "lib/assets/branding/icons"),
("languages", "lib/languages"),
], ],
) )
executables = [ executables = [
Executable( Executable(
script="main.py", script="ytsage_entry.py",
target_name=f"YTSage-v{version}", target_name=f"YTSage-v{version}",
icon="assets/branding/icons/icon.icns", icon="branding/icons/icon.icns",
) )
] ]
@@ -150,13 +168,12 @@ jobs:
options={ options={
"build_exe": build_exe_options, "build_exe": build_exe_options,
"bdist_mac": { "bdist_mac": {
"iconfile": "assets/branding/icons/icon.icns", "iconfile": "branding/icons/icon.icns",
"bundle_name": f"YTSage-v{version}", "bundle_name": f"YTSage-v{version}",
}, },
"bdist_dmg": { "bdist_dmg": {
"volume_label": f"YTSage v{version}", "volume_label": f"YTSage v{version}",
"applications_shortcut": True, "applications_shortcut": True,
# Sensible defaults; can be customized later if desired
"format": "UDZO", "format": "UDZO",
"filesystem": "HFS+", "filesystem": "HFS+",
"default_view": "icon-view", "default_view": "icon-view",
@@ -170,31 +187,79 @@ jobs:
shell: bash shell: bash
run: | run: |
source venv/bin/activate source venv/bin/activate
python setup_cxfreeze.py bdist_mac python -OO setup_cxfreeze.py bdist_mac
# Locate the built .app (cx_Freeze may place it under build/ or dist/)
- name: Trim unnecessary files from App bundle
shell: bash
run: |
version="${{ steps.get_version.outputs.VERSION }}"
# Locate the built .app
app_path="" app_path=""
for cand in "dist/YTSage-v${VERSION}.app" "build/dist/YTSage-v${VERSION}.app" "build/YTSage-v${VERSION}.app"; do for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do
if [ -d "$cand" ]; then app_path="$cand"; break; fi if [ -d "$cand" ]; then app_path="$cand"; break; fi
done done
if [ -z "$app_path" ]; then if [ -z "$app_path" ]; then
app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true)
fi fi
if [ -n "$app_path" ] && [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then
rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots"
echo "Removed screenshots folder from .app bundle at $app_path"
fi
echo "Post-bdist_mac directory listing:"
echo "-- dist --"; ls -lah dist || true
echo "-- build --"; ls -lah build || true
- name: Build DMG (bdist_dmg) if [ -n "$app_path" ]; then
shell: bash echo "Processing App Bundle at: $app_path"
run: |
source venv/bin/activate # Simplified discovery of library directories
python setup_cxfreeze.py bdist_dmg # We search for 'lib' folders and then verify if they look like the python library folder
echo "Post-bdist_dmg directory listing:" find "$app_path/Contents" -type d -name "lib" | while read -r lib_dir; do
echo "-- dist --"; ls -lah dist || true # Check if this lib dir is a python library dir (contains asyncio or PySide6 or ytsage)
echo "-- build --"; ls -lah build || true if [ -d "$lib_dir/asyncio" ] || [ -d "$lib_dir/PySide6" ] || [ -d "$lib_dir/ytsage" ]; then
echo "Cleaning Library Directory: $lib_dir"
# 1. Remove screenshots
rm -rf "$lib_dir/assets/branding/screenshots"
# 2. Remove unused Qt translations (Recursively find 'translations' folder inside PySide6)
find "$lib_dir" -type d -path "*/PySide6/*/translations" -exec rm -rf {} + 2>/dev/null || true
find "$lib_dir" -type d -path "*/PySide6/translations" -exec rm -rf {} + 2>/dev/null || true
# 3. Remove unused Qt plugins
# Determine plugins path
plugins_path=""
if [ -d "$lib_dir/PySide6/plugins" ]; then plugins_path="$lib_dir/PySide6/plugins"; fi
if [ -d "$lib_dir/PySide6/Qt/plugins" ]; then plugins_path="$lib_dir/PySide6/Qt/plugins"; fi
if [ -n "$plugins_path" ]; then
echo "Found plugins at: $plugins_path"
for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do
rm -rf "$plugins_path/$plugin"
done
# Specific cleanups
find "$plugins_path" -name "libqpdf.*" -delete
fi
# 4. Remove bloat Qt libraries / Frameworks
# Iterate specifically over unwanted Qt modules
# Matches both 'QtQml' (file) and 'QtQml.abi3.so' etc
for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets; do
find "$lib_dir" -maxdepth 2 -name "$bloat" -delete
find "$lib_dir" -maxdepth 2 -name "${bloat}.*" -delete
find "$lib_dir" -maxdepth 2 -name "*$bloat*" -delete
done
# 5. Remove build tools and unused python modules
for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do
rm -rf "$lib_dir/$tool"
done
fi
done
# Final sanity verification
echo "Trim complete. Remaining contents of lib (first 2 levels):"
find "$app_path/Contents" -type d -name "lib" -exec ls -R {} \; | head -n 50 || true
else
echo "Error: Could not find .app bundle to trim!"
exit 1
fi
- name: Package and prepare release artifacts (.app.zip and .dmg) - name: Package and prepare release artifacts (.app.zip and .dmg)
shell: bash shell: bash
@@ -203,7 +268,7 @@ jobs:
echo "Preparing artifacts for version: $version" echo "Preparing artifacts for version: $version"
mkdir -p artifacts mkdir -p artifacts
# Find the .app bundle (preferring versioned name) # Find the .app bundle
app_path="" app_path=""
for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do
if [ -d "$cand" ]; then app_path="$cand"; break; fi if [ -d "$cand" ]; then app_path="$cand"; break; fi
@@ -211,35 +276,28 @@ jobs:
if [ -z "$app_path" ]; then if [ -z "$app_path" ]; then
app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true)
fi fi
if [ -n "$app_path" ] && [ -d "$app_path" ]; then if [ -n "$app_path" ] && [ -d "$app_path" ]; then
app_base="$(basename "$app_path")" app_base="$(basename "$app_path")"
app_parent="$(dirname "$app_path")" app_parent="$(dirname "$app_path")"
# Ensure screenshots folder is not shipped
if [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then # Create ZIP
rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots"
echo "Removed screenshots folder from .app bundle at $app_path"
fi
(cd "$app_parent" && zip -r "${GITHUB_WORKSPACE}/artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" "$app_base") (cd "$app_parent" && zip -r "${GITHUB_WORKSPACE}/artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" "$app_base")
echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip"
else
echo "Warning: .app bundle not found in dist/ or build/"
fi
# Try to find the generated DMG in dist or build # Create DMG (Manual)
dmg_src="" echo "Creating DMG from $app_path..."
for cand in "dist/YTSage-v${version}.dmg"; do mkdir -p dmg_stage
if [ -f "$cand" ]; then dmg_src="$cand"; break; fi cp -R "$app_path" "dmg_stage/"
done ln -s /Applications "dmg_stage/Applications"
if [ -z "$dmg_src" ]; then
dmg_src=$(ls dist/*.dmg build/*.dmg build/dist/*.dmg 2>/dev/null | head -n1 || true)
fi
if [ -n "$dmg_src" ] && [ -f "$dmg_src" ]; then hdiutil create -volname "YTSage v${version}" -srcfolder "dmg_stage" -ov -format UDZO "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg"
# Rename the DMG to a consistent name in artifacts echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg"
cp "$dmg_src" "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg"
echo "Copied DMG to artifacts from $dmg_src -> artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" rm -rf dmg_stage
else else
echo "Warning: No DMG found in dist/ or build/" echo "Error: .app bundle not found in dist/ or build/"
exit 1
fi fi
echo "Final artifacts:" echo "Final artifacts:"
+52
View File
@@ -0,0 +1,52 @@
name: Build PyPI Package
on:
workflow_dispatch:
inputs:
version:
description: 'Version name for the release (e.g., 1.0.0)'
required: true
type: string
workflow_call:
inputs:
version:
description: 'Version name for the release (e.g., 1.0.0)'
required: true
type: string
permissions:
contents: write
jobs:
build-pypi:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build PyPI package
run: |
python build_release.py
- name: Upload Release Assets
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version || github.event.inputs.version }}
name: YTSage v${{ inputs.version || github.event.inputs.version }}
draft: true
prerelease: false
files: |
dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+204 -25
View File
@@ -49,7 +49,7 @@ jobs:
path: | path: |
venv venv
~\AppData\Local\pip\Cache ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
@@ -59,7 +59,7 @@ jobs:
python -m venv venv python -m venv venv
.\venv\Scripts\Activate.ps1 .\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --no-cache-dir -r requirements.txt pip install --no-cache-dir .
pip install --no-cache-dir cx_Freeze pip install --no-cache-dir cx_Freeze
- name: Prepare build variables - name: Prepare build variables
@@ -72,6 +72,14 @@ jobs:
- name: Create cx_Freeze setup script - name: Create cx_Freeze setup script
shell: powershell shell: powershell
run: | run: |
# Create entry point script
$entryContent = @'
from ytsage.main import main
if __name__ == "__main__":
main()
'@
Set-Content -Path "ytsage_entry.py" -Value $entryContent
# Create setup script for cx_Freeze # Create setup script for cx_Freeze
New-Item -Path "setup_cxfreeze.py" -ItemType File -Force New-Item -Path "setup_cxfreeze.py" -ItemType File -Force
Add-Content -Path "setup_cxfreeze.py" -Value "import os" Add-Content -Path "setup_cxfreeze.py" -Value "import os"
@@ -81,21 +89,30 @@ jobs:
Add-Content -Path "setup_cxfreeze.py" -Value "" Add-Content -Path "setup_cxfreeze.py" -Value ""
Add-Content -Path "setup_cxfreeze.py" -Value "build_exe_options = dict(" Add-Content -Path "setup_cxfreeze.py" -Value "build_exe_options = dict("
Add-Content -Path "setup_cxfreeze.py" -Value " optimize=2," Add-Content -Path "setup_cxfreeze.py" -Value " optimize=2,"
Add-Content -Path "setup_cxfreeze.py" -Value " silent=True,"
Add-Content -Path "setup_cxfreeze.py" -Value " packages=[" Add-Content -Path "setup_cxfreeze.py" -Value " packages=["
Add-Content -Path "setup_cxfreeze.py" -Value ' "ytsage",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtCore",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtCore",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtGui",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtGui",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWidgets",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWidgets",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtMultimedia",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtNetwork",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "requests",' Add-Content -Path "setup_cxfreeze.py" -Value ' "requests",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",' Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "markdown",' Add-Content -Path "setup_cxfreeze.py" -Value ' "markdown",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "pyglet",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "loguru",' Add-Content -Path "setup_cxfreeze.py" -Value ' "loguru",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "setuptools",' Add-Content -Path "setup_cxfreeze.py" -Value ' "setuptools",'
Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " ],"
Add-Content -Path "setup_cxfreeze.py" -Value " zip_include_packages=["
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "shiboken6",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "requests",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",'
Add-Content -Path "setup_cxfreeze.py" -Value " ],"
Add-Content -Path "setup_cxfreeze.py" -Value " excludes=[" Add-Content -Path "setup_cxfreeze.py" -Value " excludes=["
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtBluetooth",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtBluetooth",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtNetwork",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtOpenGL",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtOpenGL",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtPrintSupport",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtPrintSupport",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSvg",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSvg",'
@@ -103,7 +120,6 @@ jobs:
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtXml",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtXml",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSql",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSql",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtHelp",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtHelp",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtMultimedia",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQml",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQml",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQuick",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQuick",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWebEngineCore",' Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWebEngineCore",'
@@ -116,24 +132,26 @@ jobs:
Add-Content -Path "setup_cxfreeze.py" -Value ' "tkinter",' Add-Content -Path "setup_cxfreeze.py" -Value ' "tkinter",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "yt_dlp",' Add-Content -Path "setup_cxfreeze.py" -Value ' "yt_dlp",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "unittest",' Add-Content -Path "setup_cxfreeze.py" -Value ' "unittest",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "pydoc",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "doctest",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "email",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "test",' Add-Content -Path "setup_cxfreeze.py" -Value ' "test",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "tests",' Add-Content -Path "setup_cxfreeze.py" -Value ' "tests",'
Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " ],"
Add-Content -Path "setup_cxfreeze.py" -Value " include_files=[" Add-Content -Path "setup_cxfreeze.py" -Value " include_files=["
Add-Content -Path "setup_cxfreeze.py" -Value ' ("src", "src"),' Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/assets/Icon", "lib/assets/Icon"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/branding/icons", "lib/assets/branding/icons"),' Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/assets/sound", "lib/assets/sound"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/Icon", "lib/assets/Icon"),' Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/languages", "lib/languages"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/sound", "lib/assets/sound"),' Add-Content -Path "setup_cxfreeze.py" -Value ' ("branding/icons", "lib/assets/branding/icons"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("languages", "lib/languages"),'
Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " ],"
Add-Content -Path "setup_cxfreeze.py" -Value ")" Add-Content -Path "setup_cxfreeze.py" -Value ")"
Add-Content -Path "setup_cxfreeze.py" -Value "" Add-Content -Path "setup_cxfreeze.py" -Value ""
Add-Content -Path "setup_cxfreeze.py" -Value "executables = [" Add-Content -Path "setup_cxfreeze.py" -Value "executables = ["
Add-Content -Path "setup_cxfreeze.py" -Value " Executable(" Add-Content -Path "setup_cxfreeze.py" -Value " Executable("
Add-Content -Path "setup_cxfreeze.py" -Value ' script="main.py",' Add-Content -Path "setup_cxfreeze.py" -Value ' script="ytsage_entry.py",'
Add-Content -Path "setup_cxfreeze.py" -Value ' target_name=f"YTSage-v{version}.exe",' Add-Content -Path "setup_cxfreeze.py" -Value ' target_name=f"YTSage-v{version}.exe",'
Add-Content -Path "setup_cxfreeze.py" -Value ' base="gui",' Add-Content -Path "setup_cxfreeze.py" -Value ' base="gui",'
Add-Content -Path "setup_cxfreeze.py" -Value ' icon="assets/branding/icons/YTSage.ico",' Add-Content -Path "setup_cxfreeze.py" -Value ' icon="branding/icons/YTSage.ico",'
Add-Content -Path "setup_cxfreeze.py" -Value " )" Add-Content -Path "setup_cxfreeze.py" -Value " )"
Add-Content -Path "setup_cxfreeze.py" -Value "]" Add-Content -Path "setup_cxfreeze.py" -Value "]"
Add-Content -Path "setup_cxfreeze.py" -Value "" Add-Content -Path "setup_cxfreeze.py" -Value ""
@@ -157,13 +175,77 @@ jobs:
if (Test-Path "build\bdist.*") { Remove-Item "build\bdist.*" -Recurse -Force } if (Test-Path "build\bdist.*") { Remove-Item "build\bdist.*" -Recurse -Force }
if (Test-Path "dist") { Remove-Item "dist" -Recurse -Force } if (Test-Path "dist") { Remove-Item "dist" -Recurse -Force }
# Build executable using setup script # Build executable using setup script with extra optimization
python setup_cxfreeze.py build_exe --build-exe "dist\YTSage" python -OO setup_cxfreeze.py build_exe --build-exe "dist\YTSage"
# Remove screenshots folder to reduce build size - name: Trim unnecessary files from Standard build
if (Test-Path "dist\YTSage\lib\assets\branding\screenshots") { shell: powershell
Remove-Item "dist\YTSage\lib\assets\branding\screenshots" -Recurse -Force run: |
Write-Host "Removed screenshots folder from standard build" $distDir = "dist\YTSage"
$libDir = "$distDir\lib"
if (Test-Path $distDir) {
# Remove screenshots
if (Test-Path "$libDir\assets\branding\screenshots") {
Remove-Item "$libDir\assets\branding\screenshots" -Recurse -Force
Write-Host "Removed screenshots folder"
}
# Remove debug PDB files
Get-ChildItem -Path $distDir -Recurse -Filter "*.pdb" | Remove-Item -Force
Write-Host "Removed PDB debug files"
# Remove unused Qt translations (entire folder)
if (Test-Path "$libDir\PySide6\translations") {
Remove-Item "$libDir\PySide6\translations" -Recurse -Force
Write-Host "Removed Qt translations"
}
# Remove unused Qt plugins (based on your excludes; adjust if needed)
# Note: qpdf and qsvg plugins are removed specifically
$unusedPlugins = @("designer", "pdf", "svg", "sql", "help", "qml", "quick", "webengine", "bluetooth", "opengl", "printsupport", "test", "xml")
foreach ($plugin in $unusedPlugins) {
if (Test-Path "$libDir\PySide6\plugins\$plugin") {
Remove-Item "$libDir\PySide6\plugins\$plugin" -Recurse -Force
Write-Host "Removed unused plugin folder: $plugin"
}
}
# Explicitly remove virtualkeyboard input context if it exists
if (Test-Path "$libDir\PySide6\plugins\platforminputcontexts") {
Remove-Item "$libDir\PySide6\plugins\platforminputcontexts" -Recurse -Force
Write-Host "Removed plugin: platforminputcontexts"
}
# Explicitly remove specific image formats
foreach ($fmt in @("qpdf.dll", "qsvg.dll")) {
if (Test-Path "$libDir\PySide6\plugins\imageformats\$fmt") {
Remove-Item "$libDir\PySide6\plugins\imageformats\$fmt" -Force
Write-Host "Removed plugin: $fmt"
}
}
# Explicitly remove specific icon engines
if (Test-Path "$libDir\PySide6\plugins\iconengines\qsvgicon.dll") {
Remove-Item "$libDir\PySide6\plugins\iconengines\qsvgicon.dll" -Force
Write-Host "Removed plugin: qsvgicon.dll"
}
# Remove any duplicate or unnecessary DLLs (e.g., Qt6Qml, Qt6Quick, Qt6OpenGL from root lib)
$bloatDlls = @("Qt6Web*", "Qt6Pdf*", "Qt6Qml*", "Qt6Quick*", "Qt6VirtualKeyboard*", "Qt6OpenGL*")
foreach ($pattern in $bloatDlls) {
# Check in root lib
Get-ChildItem -Path $libDir -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force
# Check in PySide6 folder
if (Test-Path "$libDir\PySide6") {
Get-ChildItem -Path "$libDir\PySide6" -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force
}
}
Write-Host "Removed unnecessary Qt DLLs"
# Remove build tools and unused python modules
$uselessFolders = @("setuptools", "wheel", "pkg_resources", "_distutils_hack", "curses", "_pyrepl")
foreach ($folder in $uselessFolders) {
if (Test-Path "$libDir\$folder") {
Remove-Item "$libDir\$folder" -Recurse -Force
Write-Host "Removed unused module: $folder"
}
}
} }
- name: Setup FFmpeg for bundle - name: Setup FFmpeg for bundle
@@ -225,13 +307,7 @@ jobs:
(Get-Content "setup_cxfreeze.py") -replace 'YTSage-v\{version\}\.exe', 'YTSage-v{version}-ffmpeg.exe' | Set-Content "setup_cxfreeze_ffmpeg.py" (Get-Content "setup_cxfreeze.py") -replace 'YTSage-v\{version\}\.exe', 'YTSage-v{version}-ffmpeg.exe' | Set-Content "setup_cxfreeze_ffmpeg.py"
# Build executable with FFmpeg using setup script # Build executable with FFmpeg using setup script
python setup_cxfreeze_ffmpeg.py build_exe --build-exe "dist\YTSage-FFmpeg" python -OO setup_cxfreeze_ffmpeg.py build_exe --build-exe "dist\YTSage-FFmpeg"
# Remove screenshots folder to reduce build size
if (Test-Path "dist\YTSage-FFmpeg\lib\assets\branding\screenshots") {
Remove-Item "dist\YTSage-FFmpeg\lib\assets\branding\screenshots" -Recurse -Force
Write-Host "Removed screenshots folder from FFmpeg build"
}
# Copy FFmpeg binaries into dist folder post-build (more reliable than CLI include) # Copy FFmpeg binaries into dist folder post-build (more reliable than CLI include)
# Note: ffplay.exe is excluded as it's not needed by the application # Note: ffplay.exe is excluded as it's not needed by the application
@@ -251,6 +327,76 @@ jobs:
} }
} }
- name: Trim unnecessary files from FFmpeg build
shell: powershell
run: |
$distDir = "dist\YTSage-FFmpeg"
$libDir = "$distDir\lib"
if (Test-Path $distDir) {
# Remove screenshots
if (Test-Path "$libDir\assets\branding\screenshots") {
Remove-Item "$libDir\assets\branding\screenshots" -Recurse -Force
Write-Host "Removed screenshots folder"
}
# Remove debug PDB files
Get-ChildItem -Path $distDir -Recurse -Filter "*.pdb" | Remove-Item -Force
Write-Host "Removed PDB debug files"
# Remove unused Qt translations (entire folder)
if (Test-Path "$libDir\PySide6\translations") {
Remove-Item "$libDir\PySide6\translations" -Recurse -Force
Write-Host "Removed Qt translations"
}
# Remove unused Qt plugins (based on your excludes; adjust if needed)
# Note: qpdf and qsvg plugins are removed specifically
$unusedPlugins = @("designer", "pdf", "svg", "sql", "help", "qml", "quick", "webengine", "bluetooth", "opengl", "printsupport", "test", "xml")
foreach ($plugin in $unusedPlugins) {
if (Test-Path "$libDir\PySide6\plugins\$plugin") {
Remove-Item "$libDir\PySide6\plugins\$plugin" -Recurse -Force
Write-Host "Removed unused plugin folder: $plugin"
}
}
# Explicitly remove virtualkeyboard input context if it exists
if (Test-Path "$libDir\PySide6\plugins\platforminputcontexts") {
Remove-Item "$libDir\PySide6\plugins\platforminputcontexts" -Recurse -Force
Write-Host "Removed plugin: platforminputcontexts"
}
# Explicitly remove specific image formats
foreach ($fmt in @("qpdf.dll", "qsvg.dll")) {
if (Test-Path "$libDir\PySide6\plugins\imageformats\$fmt") {
Remove-Item "$libDir\PySide6\plugins\imageformats\$fmt" -Force
Write-Host "Removed plugin: $fmt"
}
}
# Explicitly remove specific icon engines
if (Test-Path "$libDir\PySide6\plugins\iconengines\qsvgicon.dll") {
Remove-Item "$libDir\PySide6\plugins\iconengines\qsvgicon.dll" -Force
Write-Host "Removed plugin: qsvgicon.dll"
}
# Remove any duplicate or unnecessary DLLs (e.g., Qt6Qml, Qt6Quick, Qt6OpenGL from root lib)
$bloatDlls = @("Qt6Web*", "Qt6Pdf*", "Qt6Qml*", "Qt6Quick*", "Qt6VirtualKeyboard*", "Qt6OpenGL*")
foreach ($pattern in $bloatDlls) {
# Check in root lib
Get-ChildItem -Path $libDir -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force
# Check in PySide6 folder
if (Test-Path "$libDir\PySide6") {
Get-ChildItem -Path "$libDir\PySide6" -Filter $pattern -ErrorAction SilentlyContinue | Remove-Item -Force
}
}
Write-Host "Removed unnecessary Qt DLLs"
# Remove build tools and unused python modules
$uselessFolders = @("setuptools", "wheel", "pkg_resources", "_distutils_hack", "curses", "_pyrepl")
foreach ($folder in $uselessFolders) {
if (Test-Path "$libDir\$folder") {
Remove-Item "$libDir\$folder" -Recurse -Force
Write-Host "Removed unused module: $folder"
}
}
}
- name: Package and prepare release artifacts (ZIPs) - name: Package and prepare release artifacts (ZIPs)
shell: powershell shell: powershell
run: | run: |
@@ -303,6 +449,39 @@ jobs:
Write-Host " Artifacts directory not found!" Write-Host " Artifacts directory not found!"
} }
- name: Create Installer (Inno Setup)
shell: powershell
run: |
$version = "${{ steps.get_version.outputs.VERSION }}"
# Install Inno Setup
choco install innosetup --no-progress
# Add matches generally C:\Program Files (x86)\Inno Setup 6
$env:Path = "C:\Program Files (x86)\Inno Setup 6;$env:Path"
# Compile
$exeName = "YTSage-v$version.exe"
Write-Host "Compiling installer for $exeName..."
# Note: Setup-windows.iss is in setup-scripts/
# OutputDir is ..\artifacts (relative to script) -> repo/artifacts
# SourceDir is ..\dist\YTSage (relative to script) -> repo/dist/YTSage
iscc /DMyAppVersion="$version" /DMyAppExeName="$exeName" /DSourceDir="..\dist\YTSage" "setup-scripts\Setup-windows.iss"
# Compile FFmpeg Installer
$exeNameFFmpeg = "YTSage-v$version-ffmpeg.exe"
Write-Host "Compiling FFmpeg installer for $exeNameFFmpeg..."
iscc /DMyAppVersion="$version" /DMyAppExeName="$exeNameFFmpeg" /DSourceDir="..\dist\YTSage-FFmpeg" "setup-scripts\Setup-windows-ffmpeg.iss"
if ($LASTEXITCODE -eq 0) {
Write-Host "Installer compilation successful."
} else {
Write-Host "Installer compilation failed."
exit 1
}
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
+6
View File
@@ -29,3 +29,9 @@ jobs:
with: with:
version: ${{ inputs.version }} version: ${{ inputs.version }}
secrets: inherit secrets: inherit
release-pypi:
uses: ./.github/workflows/build-pypi.yml
with:
version: ${{ inputs.version }}
secrets: inherit
+5
View File
@@ -0,0 +1,5 @@
include README.md
include LICENSE
include pyproject.toml
recursive-include ytsage/assets *
recursive-include ytsage/languages *
+94 -78
View File
@@ -1,13 +1,16 @@
<div align="center"> <div align="center">
<img src="assets\branding\svg\ytsage-wordmark.svg" width="400" alt="ytsage-wordmark"> <img src="branding\svg\ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="assets\branding\screenshots\main.png" width="800" alt="YTSage Interface"/> <img src="branding\screenshots\main.png" width="800" alt="YTSage Interface"/>
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=dc2626&style=for-the-badge&logo=pypi&logoColor=white)](https://badge.fury.io/py/ytsage)
[![License: MIT](https://img.shields.io/badge/License-MIT-374151?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![Downloads](https://img.shields.io/pepy/dt/ytsage?color=4b5563&style=for-the-badge&label=downloads&logo=download&logoColor=white)](https://pepy.tech/project/ytsage) [![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=dc2626&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers) [![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**A modern YouTube downloader with a clean PySide6 interface.** **A modern YouTube downloader with a clean PySide6 interface.**
Download videos in any quality, extract audio, fetch subtitles, and more. Download videos in any quality, extract audio, fetch subtitles, and more.
@@ -63,6 +66,15 @@ Install YTSage from PyPI:
pip install ytsage pip install ytsage
``` ```
<details>
<summary>🔄 Update an existing installation</summary>
```bash
pip install --upgrade ytsage
```
</details>
Then launch the app: Then launch the app:
```bash ```bash
@@ -71,6 +83,8 @@ ytsage
### 📦 Pre-built Executables ### 📦 Pre-built Executables
> [👉 Download Latest Release](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows #### 🪟 Windows
| Format | Description | | Format | Description |
@@ -80,6 +94,14 @@ ytsage
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable version, no installation required | | ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable version, no installation required |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable with FFmpeg, zipped | | ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable with FFmpeg, zipped |
<details>
<summary>🛠️ Installation Steps</summary>
1. **EXE Installer (`.exe`)**: Double-click the file and follow the setup wizard.
2. **Portable Version (`.zip`)**: Extract the archive to your desired location and run `ytsage.exe`.
3. **FFmpeg Bundled**: Choose the FFmpeg bundled versions if you don't have FFmpeg installed on your system.
</details>
#### 🐧 Linux #### 🐧 Linux
| Format | Description | | Format | Description |
@@ -87,6 +109,30 @@ ytsage
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian package | | ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian package |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, portable | | ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, portable |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM package | | ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM package |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle |
<details>
<summary>🛠️ Installation Steps</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Fix missing dependencies if any
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Follow instructions on Flathub or run:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS #### 🍎 macOS
@@ -95,10 +141,19 @@ ytsage
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Zipped application for Apple Silicon | | ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Zipped application for Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Disk image installer for Apple Silicon | | ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Disk image installer for Apple Silicon |
> [👉 Download Latest Release](https://github.com/oop7/YTSage/releases/latest) <details>
<summary>🛠️ Installation Steps</summary>
- **DMG Installer (`.dmg`)**: Double-click to mount, then drag `YTSage.app` into your Applications folder.
- **App Archive (`.zip`)**: Extract the zip and move `YTSage.app` to your Applications folder.
*Note: If you encounter an "App is damaged" error, see the [macOS troubleshooting section](#troubleshooting) below.*
</details>
---
<details> <details>
<summary>🛠️ Manual Installation from Source</summary> <summary>💻 Manual Installation from Source</summary>
### 1. Clone the Repository ### 1. Clone the Repository
@@ -112,19 +167,19 @@ cd YTSage
#### ⚡ With uv #### ⚡ With uv
```bash ```bash
uv pip install -r requirements.txt uv pip install .
``` ```
#### 📦 Or with standard pip #### 📦 Or with standard pip
```bash ```bash
pip install -r requirements.txt pip install .
``` ```
### 3. Run the Application ### 3. Run the Application
```bash ```bash
python main.py python -m ytsage.main
``` ```
</details> </details>
@@ -135,16 +190,16 @@ python main.py
<div align="center"> <div align="center">
<table> <table>
<tr> <tr>
<td><img src="assets\branding\screenshots\Download-Settings.png" alt="Download Settings" width="400"/></td> <td><img src="branding\screenshots\Download-Settings.png" alt="Download Settings" width="400"/></td>
<td><img src="assets\branding\screenshots\playlist.png" alt="Playlist Download" width="400"/></td> <td><img src="branding\screenshots\playlist.png" alt="Playlist Download" width="400"/></td>
</tr> </tr>
<tr> <tr>
<td align="center"><em>Download Settings</em></td> <td align="center"><em>Download Settings</em></td>
<td align="center"><em>Playlist Download</em></td> <td align="center"><em>Playlist Download</em></td>
</tr> </tr>
<tr> <tr>
<td><img src="assets\branding\screenshots\audio_format.png" alt="Audio Format Selection with Save Thumbnail" width="400"/></td> <td><img src="branding\screenshots\audio_format.png" alt="Audio Format Selection with Save Thumbnail" width="400"/></td>
<td><img src="assets\branding\screenshots\Custom-Option.png" alt="Custom Options" width="400"/></td> <td><img src="branding\screenshots\Custom-Option.png" alt="Custom Options" width="400"/></td>
</tr> </tr>
<tr> <tr>
<td align="center"><em>Audio Format</em></td> <td align="center"><em>Audio Format</em></td>
@@ -212,6 +267,7 @@ python main.py
c. Create a file named `cookies.txt` and paste the cookies into it c. Create a file named `cookies.txt` and paste the cookies into it
d. Select the `cookies.txt` file in the app d. Select the `cookies.txt` file in the app
- **Save Download Path:** Save the default download path for future downloads. Available in **Download Settings → Download Path**. - **Save Download Path:** Save the default download path for future downloads. Available in **Download Settings → Download Path**.
- **Output Filename Format:** Customize the output filename format using variables like `%(title)s`, `%(uploader)s`, `%(resolution)s`, etc. Available in **Download Settings → Filename Format**.
- **Updater Tab:** Unified tab in Custom Options for managing all updates: - **Updater Tab:** Unified tab in Custom Options for managing all updates:
- **yt-dlp Updates:** Check and update yt-dlp to the latest version, with release channel selection (Stable/Nightly) - **yt-dlp Updates:** Check and update yt-dlp to the latest version, with release channel selection (Stable/Nightly)
- **FFmpeg Version Checker:** Check your FFmpeg version with direct links to installation guides - **FFmpeg Version Checker:** Check your FFmpeg version with direct links to installation guides
@@ -219,9 +275,9 @@ python main.py
- **FFmpeg/yt-dlp/Deno Detection:** Automatically detect FFmpeg/yt-dlp/Deno path and version. You can use this option by clicking on about button. - **FFmpeg/yt-dlp/Deno Detection:** Automatically detect FFmpeg/yt-dlp/Deno path and version. You can use this option by clicking on about button.
- **Trim Video:** Download only specific parts of a video by specifying time ranges (HH:MM:SS format) - **Trim Video:** Download only specific parts of a video by specifying time ranges (HH:MM:SS format)
- **Proxy Support:** Use a proxy server for downloads (e.g., `http://<proxy-server>:<port>`) - **Proxy Support:** Use a proxy server for downloads (e.g., `http://<proxy-server>:<port>`)
- **Force Output Format:** Force video downloads in a specific container format (e.g., `mp4`, `webm`, `mkv`). Available in **Download Settings → Audio Format Settings**. - **Force Output Format:** Force video downloads in a specific container format (e.g., `mp4`, `webm`, `mkv`). Available in **Download Settings → Output Format Settings**.
- **Audio Format Conversion:** Convert audio-only downloads to preferred formats (`AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, or `Best`). Ideal for video editing software like DaVinci Resolve. Available in **Download Settings → Audio Format Settings**. - **Audio Format Conversion:** Convert audio-only downloads to preferred formats (`AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, or `Best`). Ideal for video editing software like DaVinci Resolve. Available in **Download Settings → Audio Format Settings**.
- **Download History:** View past downloads with thumbnails and statuses. You can use this option by clicking on download settings button. - **Download History:** View past downloads with thumbnails and statuses. You can use this option by clicking on the **History** button.
</details> </details>
@@ -342,47 +398,23 @@ YTSage/
│ │ │── build-windows.yml # Windows build workflow │ │ │── build-windows.yml # Windows build workflow
| | └── release-all.yml # Master release workflow | | └── release-all.yml # Master release workflow
│ └── 📄 CI_CD_README.md # CI/CD documentation │ └── 📄 CI_CD_README.md # CI/CD documentation
├── 📁 assets/ # Static assets and resources ├── 📁 branding/ # Branding assets (Screenshots, SVGs)
│ ├── 📁 branding/ # Branding assets │ ├── 📁 icons/ # Application icons
├── 📁 icons/ # Application icons │ ├── 📁 screenshots/ # Screenshots for documentation
│ │ ├── icon.icns # macOS icon └── 📁 svg/ # SVG assets
│ │ │ ├── icon.png # PNG icon
│ │ │ └── YTSage.ico # Windows icon
│ │ ├── 📁 screenshots/ # Screenshots for documentation
│ │ │ ├── audio_format.png
│ │ │ ├── Custom-Option.png
│ │ │ ├── Download-Settings.png
│ │ │ ├── playlist.png
│ │ │ └── main.png
│ │ └── 📁 svg/ # SVG assets
│ │ └── ytsage-wordmark.svg
│ │ └── ytsage-wordmark.svg
│ ├── 📁 Icon/ # Legacy icon directory
│ │ └── icon.png
│ └── 📁 sound/ # Audio files
│ └── notification.mp3
├── 📁 languages/ # Localization files
│ ├── 📄 ar.json # Arabic translation
│ ├── 📄 de.json # German translation
│ ├── 📄 en.json # English translation
│ ├── 📄 es.json # Spanish translation
│ ├── 📄 fr.json # French translation
│ ├── 📄 hi.json # Hindi translation
│ ├── 📄 id.json # Indonesian translation
│ ├── 📄 it.json # Italian translation
│ ├── 📄 ja.json # Japanese translation
│ ├── 📄 pl.json # Polish translation
│ ├── 📄 pt.json # Portuguese translation
│ ├── 📄 ru.json # Russian translation
│ ├── 📄 tr.json # Turkish translation
│ └── 📄 zh.json # Chinese translation
├── 📄 LICENSE # License file ├── 📄 LICENSE # License file
├── 📄 main.py # Application entry point ├── 📄 pyproject.toml # Project metadata and dependencies
├── 📄 README.md # Project documentation ├── 📄 README.md # Project documentation
├── 📄 .gitignore # Git ignore rules ├── 📄 requirements.txt # Python dependencies (dev)
── 📄 requirements.txt # Python dependencies ── 📁 ytsage/ # Source package
└── 📁 src/ # Source code ├── 📁 assets/ # Runtime assets
| │ ├── 📁 Icon/ # Application icons
│ └── 📁 sound/ # Audio files
├── 📁 languages/ # Localization files
│ ├── 📄 ar.json # Arabic translation
│ ├── 📄 de.json # German translation
│ ├── 📄 en.json # English translation
│ └── ... # Other languages
├── 📁 core/ # Core business logic ├── 📁 core/ # Core business logic
│ ├── 📄 __init__.py # Core package init │ ├── 📄 __init__.py # Core package init
│ ├── 📄 ytsage_deno.py # Deno integration │ ├── 📄 ytsage_deno.py # Deno integration
@@ -392,26 +424,14 @@ YTSage/
│ └── 📄 ytsage_yt_dlp.py # yt-dlp integration │ └── 📄 ytsage_yt_dlp.py # yt-dlp integration
├── 📁 gui/ # User interface components ├── 📁 gui/ # User interface components
│ ├── 📄 __init__.py # GUI package init │ ├── 📄 __init__.py # GUI package init
│ ├── 📄 ytsage_gui_format_table.py # Format table functionality
│ ├── 📄 ytsage_gui_main.py # Main application window │ ├── 📄 ytsage_gui_main.py # Main application window
│ ├── 📄 ytsage_gui_video_info.py # Video information display
│ └── 📁 ytsage_gui_dialogs/ # Dialog classes │ └── 📁 ytsage_gui_dialogs/ # Dialog classes
│ ├── 📄 __init__.py # Dialogs package init ├── 📁 utils/ # Utility modules
├── 📄 ytsage_dialogs_base.py # Basic dialogs │ ├── 📄 __init__.py # Utils package init
├── 📄 ytsage_dialogs_custom.py # Custom functionality dialogs │ ├── 📄 ytsage_config_manager.py # Configuration management
── 📄 ytsage_dialogs_ffmpeg.py # FFmpeg-related dialogs ── 📄 ytsage_logger.py # Logging utilities
│ ├── 📄 ytsage_dialogs_history.py # History dialogs ├── 📄 __init__.py # Package entry point
│ ├── 📄 ytsage_dialogs_selection.py # Selection dialogs └── 📄 main.py # Main execution script
│ ├── 📄 ytsage_dialogs_settings.py # Settings dialogs
│ ├── 📄 ytsage_dialogs_update.py # Update dialogs
│ └── 📄 ytsage_dialogs_updater.py # Updater dialogs
└── 📁 utils/ # Utility modules
├── 📄 __init__.py # Utils package init
├── 📄 ytsage_config_manager.py # Configuration management
├── 📄 ytsage_constants.py # Application constants
├── 📄 ytsage_history_manager.py # History management
├── 📄 ytsage_localization.py # Localization utilities
└── 📄 ytsage_logger.py # Logging utilities
``` ```
</details> </details>
@@ -480,10 +500,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
<td><a href="https://python-markdown.github.io/">markdown</a></td> <td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown Rendering</td> <td>Markdown Rendering</td>
</tr> </tr>
<tr>
<td><a href="https://pyglet.org/">pyglet</a></td>
<td>Audio Playback</td>
</tr>
<tr> <tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td> <td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td> <td>Logging</td>

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before

Width:  |  Height:  |  Size: 611 KiB

After

Width:  |  Height:  |  Size: 611 KiB

Before

Width:  |  Height:  |  Size: 669 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

Before

Width:  |  Height:  |  Size: 748 KiB

After

Width:  |  Height:  |  Size: 748 KiB

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 578 KiB

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 826 B

+64
View File
@@ -0,0 +1,64 @@
import os
import shutil
import re
import subprocess
import sys
def build_release():
print("Starting Production Build...")
# 1. Backup local README
if os.path.exists("README.md"):
shutil.copy2("README.md", "README.md.bak")
print("Backed up README.md")
try:
# 2. Prepare PyPI README
with open("README.md", "r", encoding="utf-8") as f:
content = f.read()
# Base URL for assets
base_url = "https://github.com/oop7/YTSage/raw/main/branding/"
# Function to fix paths matches by regex
def path_fixer(match):
full_match = match.group(0)
# Convert backslashes to forward slashes for URL compatibility
fixed = full_match.replace("\\", "/")
# Prepend the absolute URL
return fixed.replace("branding/", base_url)
# Regex: matches "branding" followed by backslash or slash, then characters until quote, closing paren, or space
# This captures paths like: branding\screenshots\main.png
pattern = r"branding[\\/][^\"')\s]+"
new_content = re.sub(pattern, path_fixer, content)
with open("README.md", "w", encoding="utf-8") as f:
f.write(new_content)
print("Modified README.md for PyPI (Absolute URLs)")
# 3. Clean previous builds
for folder in ["dist", "build", "ytsage.egg-info"]:
if os.path.exists(folder):
shutil.rmtree(folder)
# 4. Run Build
print("Building Wheel...")
subprocess.check_call([sys.executable, "-m", "build"])
except Exception as e:
print(f"Error during build: {e}")
sys.exit(1)
finally:
# 5. Restore README
if os.path.exists("README.md.bak"):
# Force move back, overwriting the modified one
shutil.move("README.md.bak", "README.md")
print("Restored original README.md")
print("Build Complete. Artifacts are in the /dist folder.")
if __name__ == "__main__":
build_release()
-51
View File
@@ -1,51 +0,0 @@
import sys
from PySide6.QtWidgets import QApplication, QMessageBox
from src.utils.ytsage_logger import logger
from src.core.ytsage_yt_dlp import check_ytdlp_binary, setup_ytdlp # Import the new yt-dlp setup functions
from src.core.ytsage_deno import check_deno_binary, setup_deno # Import the new Deno setup functions
from src.gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main
def show_error_dialog(message):
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setText("Application Error")
error_dialog.setInformativeText(message)
error_dialog.setWindowTitle("Error")
error_dialog.exec()
def main():
try:
logger.info("Starting YTSage application")
app = QApplication(sys.argv)
# Get the expected binary path and check if it exists
if not check_ytdlp_binary():
# No app-specific binary found, show setup dialog regardless of Python package
logger.warning("No yt-dlp binary found, starting setup process")
yt_dlp_path = setup_ytdlp()
if yt_dlp_path == "yt-dlp": # If user canceled or something went wrong
logger.warning("yt-dlp not configured properly")
# Check for Deno binary
if not check_deno_binary():
logger.warning("No Deno binary found, starting setup process")
deno_path = setup_deno()
if deno_path == "deno": # If user canceled or something went wrong
logger.warning("Deno not configured properly")
window = YTSageApp() # Instantiate the main application class
window.show()
logger.info("Application window shown, entering main loop")
sys.exit(app.exec())
except Exception as e:
logger.critical(f"Critical application error: {e}", exc_info=True)
show_error_dialog(f"Critical error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+68
View File
@@ -0,0 +1,68 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "ytsage"
version = "5.0.0b5"
description = "Modern YouTube downloader with a clean PySide6 interface."
authors = [
{ name = "oop7", email = "oop7_support@proton.me" },
]
dependencies = [
"PySide6>=6.10.1",
"requests>=2.32.5",
"pillow>=12.0.0",
"packaging>=25.0",
"markdown>=3.10",
"loguru>=0.7.3",
"setuptools>=80.9.0",
]
requires-python = ">=3.10,<3.15"
readme = "README.md"
license = "MIT"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
'Programming Language :: Python :: 3.14',
"Intended Audience :: End Users/Desktop",
"Operating System :: OS Independent",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Desktop Environment",
"Environment :: X11 Applications :: Qt",
"Environment :: Win32 (MS Windows)",
"Environment :: MacOS X"
]
keywords = ["youtube", "downloader", "video", "audio", "PySide6", "yt-dlp", "GUI"]
[project.scripts]
ytsage = "ytsage.main:main"
[project.urls]
Homepage = "https://github.com/oop7/YTSage"
Bug-Tracker = "https://github.com/oop7/YTSage/issues"
Reddit = "https://www.reddit.com/r/NO-N_A_M_E/"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
include = ["ytsage*"]
[tool.setuptools.package-data]
ytsage = [
"assets/Icon/icon.png",
"assets/sound/notification.mp3",
"languages/*.json",
]
-8
View File
@@ -1,8 +0,0 @@
PySide6>=6.10.1
requests>=2.32.5
pillow>=12.0.0
packaging>=25.0
markdown>=3.10
pyglet>=2.1.11
loguru>=0.7.3
setuptools>=80.9.0
@@ -0,0 +1,418 @@
; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***
;
; To download user-contributed translations of this file, go to:
; https://jrsoftware.org/files/istrans/
;
; Note: When translating this text, do not add periods (.) to the end of
; messages that didn't have them already, because on those messages Inno
; Setup adds the periods automatically (appending a period would result in
; two periods being displayed).
;
; Maintained by Zhenghan Yang
; Email: 847320916@QQ.com
; Translation based on network resource
; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
;
[LangOptions]
; The following three entries are very important. Be sure to read and
; understand the '[LangOptions] section' topic in the help file.
LanguageName=简体中文
; If Language Name display incorrect, uncomment next line
; LanguageName=<7B80><4F53><4E2D><6587>
; About LanguageID, to reference link:
; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
LanguageID=$0804
; About CodePage, to reference link:
; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
LanguageCodePage=936
; If the language you are translating to requires special font faces or
; sizes, uncomment any of the following entries and change them accordingly.
;DialogFontName=
;DialogFontSize=9
;DialogFontBaseScaleWidth=7
;DialogFontBaseScaleHeight=15
;WelcomeFontName=Segoe UI
;WelcomeFontSize=14
[Messages]
; *** 应用程序标题
SetupAppTitle=安装
SetupWindowTitle=安装 - %1
UninstallAppTitle=卸载
UninstallAppFullTitle=%1 卸载
; *** Misc. common
InformationTitle=信息
ConfirmTitle=确认
ErrorTitle=错误
; *** SetupLdr messages
SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
HelpTextNote=
; *** 启动错误消息
LastErrorMessage=%1。%n%n错误 %2: %3
SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
InvalidParameter=无效的命令行参数:%n%n%1
SetupAlreadyRunning=安装程序正在运行。
WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
NotOnThisPlatform=此程序不能在 %1 上运行。
OnlyOnThisPlatform=此程序只能在 %1 上运行。
OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。
SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
; *** 启动问题
PrivilegesRequiredOverrideTitle=选择安装程序模式
PrivilegesRequiredOverrideInstruction=选择安装模式
PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
PrivilegesRequiredOverrideText2=%1 可以仅为您安装,或为所有用户安装(需要管理员权限)。
PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)
PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)
PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项)
; *** 其他错误
ErrorCreatingDir=安装程序无法创建目录“%1”
ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
; *** 安装程序公共消息
ExitSetupTitle=退出安装程序
ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
AboutSetupMenuItem=关于安装程序(&A)...
AboutSetupTitle=关于安装程序
AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
AboutSetupNote=
TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
; *** 按钮
ButtonBack=< 上一步(&B)
ButtonNext=下一步(&N) >
ButtonInstall=安装(&I)
ButtonOK=确定
ButtonCancel=取消
ButtonYes=是(&Y)
ButtonYesToAll=全是(&A)
ButtonNo=否(&N)
ButtonNoToAll=全否(&O)
ButtonFinish=完成(&F)
ButtonBrowse=浏览(&B)...
ButtonWizardBrowse=浏览(&R)...
ButtonNewFolder=新建文件夹(&M)
; *** “选择语言”对话框消息
SelectLanguageTitle=选择安装语言
SelectLanguageLabel=选择安装时使用的语言。
; *** 公共向导文字
ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
BeveledLabel=
BrowseDialogTitle=浏览文件夹
BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
NewFolderName=新建文件夹
; *** “欢迎”向导页
WelcomeLabel1=欢迎使用 [name] 安装向导
WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。
; *** “密码”向导页
WizardPassword=密码
PasswordLabel1=这个安装程序有密码保护。
PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
PasswordEditLabel=密码(&P)
IncorrectPassword=您输入的密码不正确,请重新输入。
; *** “许可协议”向导页
WizardLicense=许可协议
LicenseLabel=请在继续安装前阅读以下重要信息。
LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
LicenseAccepted=我同意此协议(&A)
LicenseNotAccepted=我不同意此协议(&D)
; *** “信息”向导页
WizardInfoBefore=信息
InfoBeforeLabel=请在继续安装前阅读以下重要信息。
InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
WizardInfoAfter=信息
InfoAfterLabel=请在继续安装前阅读以下重要信息。
InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
; *** “用户信息”向导页
WizardUserInfo=用户信息
UserInfoDesc=请输入您的信息。
UserInfoName=用户名(&U)
UserInfoOrg=组织(&O)
UserInfoSerial=序列号(&S)
UserInfoNameRequired=您必须输入用户名。
; *** “选择目标目录”向导页
WizardSelectDir=选择目标位置
SelectDirDesc=您想将 [name] 安装在哪里?
SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share
InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
DiskSpaceWarningTitle=磁盘空间不足
DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
DirNameTooLong=文件夹名称或路径太长。
InvalidDirName=文件夹名称无效。
BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
DirExistsTitle=文件夹已存在
DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
DirDoesntExistTitle=文件夹不存在
DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
; *** “选择组件”向导页
WizardSelectComponents=选择组件
SelectComponentsDesc=您想安装哪些程序组件?
SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
FullInstallation=完全安装
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
CompactInstallation=简洁安装
CustomInstallation=自定义安装
NoUninstallWarningTitle=组件已存在
NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
; *** “选择附加任务”向导页
WizardSelectTasks=选择附加任务
SelectTasksDesc=您想要安装程序执行哪些附加任务?
SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
; *** “选择开始菜单文件夹”向导页
WizardSelectProgramGroup=选择开始菜单文件夹
SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
MustEnterGroupName=您必须输入一个文件夹名。
GroupNameTooLong=文件夹名或路径太长。
InvalidGroupName=无效的文件夹名字。
BadGroupName=文件夹名不能包含下列任何字符:%n%n%1
NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
; *** “准备安装”向导页
WizardReady=准备安装
ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。
ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
ReadyLabel2b=点击“安装”继续此安装程序。
ReadyMemoUserInfo=用户信息:
ReadyMemoDir=目标位置:
ReadyMemoType=安装类型:
ReadyMemoComponents=已选择组件:
ReadyMemoGroup=开始菜单文件夹:
ReadyMemoTasks=附加任务:
; *** TExtractionWizardPage 向导页面与 ExtractArchive
ExtractingLabel=正在解压文件...
ButtonStopExtraction=停止解压(&S)
StopExtraction=您确定要停止解压吗?
ErrorExtractionAborted=解压已中止
ErrorExtractionFailed=解压失败:%1
; *** 压缩文件解压失败详情
ArchiveIncorrectPassword=压缩文件密码不正确
ArchiveIsCorrupted=压缩文件已损坏
ArchiveUnsupportedFormat=不支持的压缩文件格式
; *** TDownloadWizardPage 向导页面和 DownloadTemporaryFile
DownloadingLabel2=正在下载文件...
ButtonStopDownload=停止下载(&S)
StopDownload=您确定要停止下载吗?
ErrorDownloadAborted=下载已中止
ErrorDownloadFailed=下载失败:%1 %2
ErrorDownloadSizeFailed=获取下载大小失败:%1 %2
ErrorProgress=无效的进度:%1 / %2
ErrorFileSize=文件大小错误:预期 %1,实际 %2
; *** “正在准备安装”向导页
WizardPreparing=正在准备安装
PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。
PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。
CannotContinue=安装程序不能继续。请点击“取消”退出。
ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
CloseApplications=自动关闭应用程序(&A)
DontCloseApplications=不要关闭应用程序(&D)
ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动?
; *** “正在安装”向导页
WizardInstalling=正在安装
InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。
; *** “安装完成”向导页
FinishedHeadingLabel=[name] 安装完成
FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。
FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
ClickFinish=点击“完成”退出安装程序。
FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗?
FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗?
ShowReadmeCheck=是,我想查阅自述文件
YesRadio=是,立即重启电脑(&Y)
NoRadio=否,稍后重启电脑(&N)
; used for example as 'Run MyProg.exe'
RunEntryExec=运行 %1
; used for example as 'View Readme.txt'
RunEntryShellExec=查阅 %1
; *** “安装程序需要下一张磁盘”提示
ChangeDiskTitle=安装程序需要下一张磁盘
SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
PathLabel=路径(&P)
FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
SelectDirectoryLabel=请指定下一张磁盘的位置。
; *** 安装阶段消息
SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
AbortRetryIgnoreSelectAction=选择操作
AbortRetryIgnoreRetry=重试(&T)
AbortRetryIgnoreIgnore=忽略错误并继续(&I)
AbortRetryIgnoreCancel=关闭安装程序
RetryCancelSelectAction=选择操作
RetryCancelRetry=重试(&T)
RetryCancelCancel=取消(&C)
; *** 安装状态消息
StatusClosingApplications=正在关闭应用程序...
StatusCreateDirs=正在创建目录...
StatusExtractFiles=正在提取文件...
StatusDownloadFiles=正在下载文件...
StatusCreateIcons=正在创建快捷方式...
StatusCreateIniEntries=正在创建 INI 条目...
StatusCreateRegistryEntries=正在创建注册表条目...
StatusRegisterFiles=正在注册文件...
StatusSavingUninstall=正在保存卸载信息...
StatusRunProgram=正在完成安装...
StatusRestartingApplications=正在重启应用程序...
StatusRollback=正在撤销更改...
; *** 其他错误
ErrorInternal2=内部错误:%1
ErrorFunctionFailedNoCode=%1 失败
ErrorFunctionFailed=%1 失败;错误代码 %2
ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
ErrorExecutingProgram=无法执行文件:%n%1
; *** 注册表错误
ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
; *** INI 错误
ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
; *** 文件复制错误
FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)
FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)
SourceIsCorrupted=源文件已损坏
SourceDoesntExist=源文件“%1”不存在
SourceVerificationFailed=源文件验证失败: %1
VerificationSignatureDoesntExist=签名文件“%1”不存在
VerificationSignatureInvalid=签名文件“%1”无效
VerificationKeyNotFound=签名文件“%1”使用了未知密钥
VerificationFileNameIncorrect=文件名不正确
VerificationFileTagIncorrect=文件标签不正确
VerificationFileSizeIncorrect=文件大小不正确
VerificationFileHashIncorrect=文件哈希值不正确
ExistingFileReadOnly2=无法替换现有文件,它是只读的。
ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
ErrorReadingExistingDest=尝试读取现有文件时出错:
FileExistsSelectAction=选择操作
FileExists2=文件已经存在。
FileExistsOverwriteExisting=覆盖已存在的文件(&O)
FileExistsKeepExisting=保留现有的文件(&K)
FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
ExistingFileNewerSelectAction=选择操作
ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)
ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
ErrorCreatingTemp=尝试在目标目录创建文件时出错:
ErrorReadingSource=尝试读取下列源文件时出错:
ErrorCopying=尝试复制下列文件时出错:
ErrorDownloading=下载文件时出错:
ErrorExtracting=解压压缩文件时出错:
ErrorReplacingExistingFile=尝试替换现有文件时出错:
ErrorRestartReplace=重启并替换失败:
ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
ErrorRegisterServer=无法注册 DLL/OCX%1
ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
ErrorRegisterTypeLib=无法注册类库:%1
; *** 卸载显示名字标记
; used for example as 'My Program (32-bit)'
UninstallDisplayNameMark=%1 (%2)
; used for example as 'My Program (32-bit, All users)'
UninstallDisplayNameMarks=%1 (%2, %3)
UninstallDisplayNameMark32Bit=32 位
UninstallDisplayNameMark64Bit=64 位
UninstallDisplayNameMarkAllUsers=所有用户
UninstallDisplayNameMarkCurrentUser=当前用户
; *** 安装后错误
ErrorOpeningReadme=尝试打开自述文件时出错。
ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。
; *** 卸载消息
UninstallNotFound=文件“%1”不存在。无法卸载。
UninstallOpenError=文件“%1”不能被打开。无法卸载。
UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)
ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。
UninstalledAll=已顺利从您的电脑中移除 %1。
UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗?
UninstallDataCorrupted=文件“%1”已损坏。无法卸载
; *** 卸载状态消息
ConfirmDeleteSharedFileTitle=删除共享的文件吗?
ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。
SharedFileNameLabel=文件名:
SharedFileLocationLabel=位置:
WizardUninstalling=卸载状态
StatusUninstalling=正在卸载 %1...
; *** Shutdown block reasons
ShutdownBlockReasonInstallingApp=正在安装 %1。
ShutdownBlockReasonUninstallingApp=正在卸载 %1。
; The custom messages below aren't used by Setup itself, but if you make
; use of them in your scripts, you'll want to translate them.
[CustomMessages]
NameAndVersion=%1 版本 %2
AdditionalIcons=附加快捷方式:
CreateDesktopIcon=创建桌面快捷方式(&D)
CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
ProgramOnTheWeb=%1 网站
UninstallProgram=卸载 %1
LaunchProgram=运行 %1
AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
AutoStartProgramGroupDescription=启动:
AutoStartProgram=自动启动 %1
AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
@@ -0,0 +1,336 @@
; *** Inno Setup version 5.5.3+ Hindi messages ***
; Translated by Him Prasad Gautam [ drishtibachak at gmail.com ]
; To download user-contributed translations of this file, go to:
; http://www.jrsoftware.org/files/istrans/
;
; Note: When translating this text, do not add periods (.) to the end of
; messages that didn't have them already, because on those messages Inno
; Setup adds the periods automatically (appending a period would result in
; two periods being displayed).
[LangOptions]
; The following three entries are very important. Be sure to read and
; understand the '[LangOptions] section' topic in the help file.
LanguageName=<0939><093F><0902><0926><0940>
LanguageID=$0439
LanguageCodePage=0
; If the language you are translating to requires special font faces or
; sizes, uncomment any of the following entries and change them accordingly.
;DialogFontName=
;DialogFontSize=10
;WelcomeFontName=
WelcomeFontSize=12
;TitleFontName=
TitleFontSize=35
;CopyrightFontName=
CopyrightFontSize=9
[Messages]
; *** Application titles
SetupAppTitle=स्थापना
SetupWindowTitle=स्थापना - %1
UninstallAppTitle=निस्कासन
UninstallAppFullTitle=%1 कि निस्कासन
; *** Misc. common
InformationTitle=सुचना
ConfirmTitle=पुष्टिकरण
ErrorTitle=त्रुटी
; *** SetupLdr messages
SetupLdrStartupMessage=इस से %1 आपकि कल्पयन्त्र में अधिष्ठापन होगा. क्या आप आगे बढ़ना चाहते है?
LdrCannotCreateTemp=अस्थाई फ़ाइल नही बना पा रहा. स्थापना को बिच में ही रोकना पड़ा.
LdrCannotExecTemp=अस्थाई फोल्डर में से फ़ाइल कार्यान्वयन नही कर पाया. स्थापना को बिच में ही रोकना पड़ा.
; *** Startup error messages
LastErrorMessage=%1.%n%nत्रुटी %2: %3
SetupFileMissing=फ़ाइल %1 अधिष्ठापन सङ्ग्रहिका में नही है. कृपया या तो समस्या का निदान कीजिये या कार्यक्रम की नई प्रति लाइए.
SetupFileCorrupt=स्थापना फाइल में त्रुटी है. कृपया नई कार्यक्रम की प्रति लाइए.
SetupFileCorruptOrWrongVer=स्थापना फाइल में त्रुटी है या तो अलग प्रकार कि है. कृपया समस्या-निदान करे या कार्यक्रम की नई प्रति लाइए.
InvalidParameter=फोल्डर का नाम वैध नही है.
SetupAlreadyRunning=स्थापना तो पहले से हि चल रहा है
WindowsVersionNotSupported=इस से [name/ver] आपके कल्पयन्त्र में अधिष्ठापन होगा.%n%nये बहेतर होगा आगे बढने से पहेले आप अन्य सभी कार्यक्रम हाल तुरत के लिए बंध कर दे.
WindowsServicePackRequired=ये कार्यक्रम को %1 Service Pack %2 या पिछला संस्करण चाहिए.
NotOnThisPlatform=ये कार्यक्रम %1 पे नही चलेगा.
OnlyOnThisPlatform=ये कार्यक्रम केवल %1 पे ही चलेगा.
OnlyOnTheseArchitectures=ये कार्यक्रम केवल इन प्रोसेसर :%n%n%1 से अनुरूप विन्डोज़ प्लेटफॉर्म पे ही चलेगा.
MissingWOW64APIs=आपका विंडो प्लेटफॉर्म 64-bit अधिष्ठापन समर्थन नही करता. कृपया सर्विस पैक %1 अधिष्ठापन करे.
WinVersionTooLowError=ये कार्यक्रम चलने के लिए %1 संस्करण %2 या उस से पिछला चाहिए.
WinVersionTooHighError=ये कार्यक्रम नही अधिष्ठापन किया जा सकता %1 संस्करण %2 या पिछला पे.
AdminPrivilegesRequired=अगर आप प्रशासक खाते से आरम्भ करे तो ही ये कार्यक्रम अधिष्ठापन कर पाओगे.
PowerUserPrivilegesRequired=आप प्रशासक खाते या शक्ति-प्रयोग कर्ता समूह के खाते से आरम्भ करे तो ही ये कार्यक्रम अधिष्ठापन कर पाओगे.
SetupAppRunningError=स्थापना ने पकड़ा की %1 हाल चालू है..%n%n कृपया उसे बंध करे अभी, और बाद में आगे बढने वास्ते ठीक या निकल जाने वास्ते रद्द करेँ पे क्लिक करे.
UninstallAppRunningError=निस्कासन को ये ज्ञात हुआ की %1 अभी चालू है.%n%n कृपया उसे बंध करे और फिर आगे बढने के लिए ठीक या बाहर जाने के लिए रद्द करेँ पे क्लिक करे.
; *** Misc. errors
ErrorCreatingDir=स्थापना "%1" सङ्ग्रहिका बनाने में विफल रहा
ErrorTooManyFilesInDir=%1 सङ्ग्रहिका में बहुत फाइल मौजूद होने के वजह से स्थापना फाइल बनाने में विफल रहा.
; *** Setup common messages
ExitSetupTitle=स्थापना कि बहिर्गमन
ExitSetupMessage=स्थापना कि कार्य पूर्ण नही हुआ, यदि आप अभी बाहर जाने कि ईराधा करेंगे तो कार्यक्रम सहि ढंग से अधिष्ठापन नही होगा.%n%nआप किसी ओर वक्त फिर से अधिष्ठापन कर सकते हो.%n%nक्या बाहर जाए?
AboutSetupMenuItem=स्थापना के बारे में...
AboutSetupTitle=स्थापना के बारे में
AboutSetupMessage=%1 संस्करण %2%n%3%n%n%1 गृह पृष्ठ:%n%4
AboutSetupNote=
TranslatorNote= यह हिन्दी में अनुवाद कि कार्य हिम प्रसाद गौतम ने किया है.
; *** Buttons
ButtonBack=< &पिछे हटो
ButtonNext=&आगे बढो >
ButtonInstall=&अधिष्ठापन
ButtonOK=&ठीक
ButtonCancel=&रद्द करेँ
ButtonYes=&हाँ
ButtonYesToAll=&सभी के लिए हाँ
ButtonNo=&नही
ButtonNoToAll=स&भी के लिए नही
ButtonFinish=&समाप्त
ButtonBrowse=&ब्राउज़...
ButtonWizardBrowse=&ब्राउज़...
ButtonNewFolder=&नया फोल्डर बनाए
; "Select Language" dialog messages
SelectLanguageTitle=स्थापना भाषा चयन
SelectLanguageLabel=अधिष्ठापन के दरम्यान इस्तेमाल होने वाली भाषा चयन करे:
; *** Common wizard text
ClickNext=आगे बढने के लिए आगे बढो दबाए, या बाहर जाने के वास्ते रद्द करेँ दबाए.
BeveledLabel= सौजन्यः हिम प्रसाद गौतम
BrowseDialogTitle=फोल्डर के लिए ब्राउज़ करे
BrowseDialogLabel=नीचे की सुची में से एक फोल्डर चयन करके ठीक दबाए.
NewFolderName=नया फोल्डर
; "Welcome" wizard page
WelcomeLabel1=यह [name] कि स्थापना हो रही समारोह में आपका स्वागत है
WelcomeLabel2=इस से [name/ver] आपके कल्पयन्त्र में अधिष्ठापन होगा.%n%nये बहेतर होगा आगे बढने से पहले आप अन्य सभी खुलि हुई कार्यक्रम हाल के लिए बंध कर दे.
; "Password" wizard page
WizardPassword=खुपियाशब्द
PasswordLabel1=ये अधिष्ठापन खुपियाशब्द से लोक है.
PasswordLabel3=कृपया खुपियाशब्द लिखें और बाद में 'आगे बढो' बटन दबाए. खुपियाशब्द case-सम्वेदनसील है.
PasswordEditLabel=खुपियाशब्द:
IncorrectPassword=आपने लिखा हुआ खुपियाशब्द गलत है. कृपया फिर से कोशिश करे.
; "License Agreement" wizard page
WizardLicense=इजाजत करार
LicenseLabel=आगे बढने से पहेले ये महत्वपूर्ण सूचनाए पढे.
LicenseLabel3=ये इजाजत करार पढे. आगे बढने से पहेले आपको इसकी शर्तों को मानना ही होगा.
LicenseAccepted=हाँ मुझे ये करारनामा कबूल है.
LicenseNotAccepted=नही मुझे ये करारनामा कबूल नही है.
; "Information" wizard pages
WizardInfoBefore=सुचना
InfoBeforeLabel=आगे बढने से पहेले ये महत्वपूर्ण सूचनाए पढे.
InfoBeforeClickLabel=जब आप तयार हो, 'आगे बढो' बटन दबाए.
WizardInfoAfter=सुचना
InfoAfterLabel=आगे बढने से पहेले ये महत्वपूर्ण सूचनाए पढे.
InfoAfterClickLabel=जब आप तयार हो, 'आगे बढो' बटन दबाए.
; "User Information" wizard page
WizardUserInfo=प्रयोग कर्ता की जानकारी
UserInfoDesc=कृपया आपकी जानकारी अंदर डाले.
UserInfoName=प्रयोग कर्ता का नाम:
UserInfoOrg=संस्था:
UserInfoSerial=क्रमाङ्क
UserInfoNameRequired=आपको नाम तो डालना ही होगा.
; "Select Destination Location" wizard page
WizardSelectDir=लक्ष्य पथ चयन करे
SelectDirDesc=[name] को किधर अधिष्ठापन करना है?
SelectDirLabel3=स्थापना [name] को निम्नलिखित फोल्डर में डालेगा.
SelectDirBrowseLabel=आगे बढने वास्ते आगे बढो दबाए. यदि अन्य फोल्डर चयन करना है तो ब्राउज़ दबाए.
DiskSpaceMBLabel=कमसेकम [mb] MB जितनी जगह तो जरूरी होगी.
CannotInstallToNetworkDrive=श्थापना ने नेटवर्क ड्राइभ नहि रख पाया.
CannotInstallToUNCPath=स्थापना ने UNC path नहि रख पाया.
InvalidPath=आपको ड्राइव अक्षर के साथ पूर्ण पथ देना होगा उदाहरण:%n%nC:\APP%n%n या तो UNC रास्ता यह रूप में:%n%n\\server\share
InvalidDrive=जो drive या UNC share आपने चयन की है उस मे हम पहुँच नही कर पा रहे कृपया अन्य चयन करे.
DiskSpaceWarningTitle=जरूरी जगह नही है.
DiskSpaceWarning=स्थापना कम से कम %1 KB जगह मंगता है, लेकिन चयनित ड्राइव में तो केवल %2 KB ही मौजूद है.%n%nक्या आप फिर भी आगे बढ़ना चाहते हो?
DirNameTooLong=फोल्डर का नाम या पथ बहोत लंबा है.
InvalidDirName=फोल्डर का नाम वैध नही है.
BadDirName32=फोल्डर नाम में ये अक्षर नही इस्तेमाल कर सकते:%n%n%1
DirExistsTitle=फोल्डर मौजूद है
DirExists=फोल्डर:%n%n%1%n%nपहेले से ही मौजूद है, क्या आप फिर भी उसमे अधिष्ठापन करना चाहते है?
DirDoesntExistTitle=फोल्डर मौजूद नही है
DirDoesntExist=फोल्डर:%n%n%1%n%nमौजूद नही है. क्या आप ये फोल्डर बनाना चाहते है?
; "Select Components" wizard page
WizardSelectComponents=सहयोगियोँ पसंद करे.
SelectComponentsDesc=कोनसे सहयोगियोँ अधिष्ठापन करने है?
SelectComponentsLabel2=जो सहयोगियोँ अधिष्ठापन करना है, उन्हें चयन करे; जिन्हें नही करना हो तो उन्हें साफ करे. जब आगे बढने के लिए तयार हो तो आगे बढो दबाए.
FullInstallation=सम्पूर्ण अधिष्ठापन
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
CompactInstallation=मजबुत अधिष्ठापन
CustomInstallation=रिवाजी अधिष्ठापन.
NoUninstallWarningTitle=सहयोगियोँ मौजूद है.
NoUninstallWarning=स्थापना को ये ज्ञात हुआ है की निम्नलिखित सहयोगियोँ पहेले से ही मोजूद है.:%n%n%1%n%nइन्हें डी-चयन करने से वे निस्कासन नही होगे.%n%nक्या आप ऐसे ही आगे बढ़ना चाहते है?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
ComponentsDiskSpaceMBLabel=इस चयन के साथ स्थापना वास्ते [mb] MB जगह चाहिए.
; "Select Additional Tasks" wizard page
WizardSelectTasks=अतिरिक्त काम चयन करे.
SelectTasksDesc=कोन से अतिरिक्त काम करने है?
SelectTasksLabel2=[name] को अधिष्ठापन करते वक्त जो अतिरिक्त काम करने है उन्हें चयन करे और बाद में आगे बढो पे क्लिक करे.
; "Select Start Menu Folder" wizard page
WizardSelectProgramGroup=सुरु मेनू फोल्डर चयन करे.
SelectStartMenuFolderDesc=कार्यक्रम के छोटीरास्ता किधर रखने है?
SelectStartMenuFolderLabel3=स्थापना कार्यक्रम के छोटीरास्ता निम्नलिखित सुरु-मेनू फोल्डर में डालेगा.
SelectStartMenuFolderBrowseLabel=आगे बढने के लिए आगे बढो दबाए. यदि अलग फोल्डर में अधिष्ठापन करना है तो Browse दबाए.
MustEnterGroupName=आपको फोल्डर का नाम तो डालना ही होगा.
GroupNameTooLong=फोल्डर का नाम या पथ बहुत लंबा है.
InvalidGroupName=फोल्डर का नाम वैध नही है.
BadGroupName=फोल्डर नाम में ये वाले अक्षर नही डाल सकते:%n%n%1
NoProgramGroupCheck2=सुरु मेनू फोल्डर नही बनाना है.
; "Ready to Install" wizard page
WizardReady=अधिष्ठापन के लिए तयार
ReadyLabel1=स्थापना अब [name] को आपके कल्पयन्त्रमें अधिष्ठापन करने के लिए तयार है.
ReadyLabel2a=आगे बढने के लिए अधिष्ठापन दबाए, अगर कोई बदलाव करना है तो पिछे हटो दबाए.
ReadyLabel2b=अधिष्ठापन में आगे बढने के लिए अधिष्ठापन दबाए.
ReadyMemoUserInfo=प्रयोग कर्ता की सूचनाए:
ReadyMemoDir=लक्ष्य सङ्ग्रहिका:
ReadyMemoType=स्थापना का प्रकार:
ReadyMemoComponents=चयन किये सहयोगियों:
ReadyMemoGroup=सुरु मेनू फोल्डर:
ReadyMemoTasks=अतिरिक्त काम:
; "Preparing to Install" wizard page
WizardPreparing=अधिष्ठापन के लिए तैयारी कर रहा है.
PreparingDesc=स्थापना [name] को आपके कल्पयन्त्र में डालने की तैयारीकर रहा है.
PreviousInstallNotCompleted=पिछले कार्यक्रम का प्रतिस्थापन / अधिष्ठापन सही ढंग से पूरा नही हुआ था. आपको कल्पयन्त्र फिर सुरु करना पडेगा.%n%nकल्पयन्त्र फिर सुरु करने पश्चात आप फिर से [name] का अधिष्ठापन शुरू करे.
CannotContinue=स्थापना आगे नही बढ़ सकता, कृपया रद्द करेँ बटन दबाएँ.
ApplicationsFound=निचे वाली अनुप्रयोगो ने स्थापना द्वारा अपडेट किया जाने वाला फाइलों को इस्तेमाल किया है. आपको यह मसवरा दिया जा ता है कि आप स्थापना को यह अनुप्रयोगौं कि खुद ही बन्द करने कि इजाजत प्रदान करें.
ApplicationsFound2=यह अनुप्रयोगो ने स्थापना द्वारा अपडेट किया जाने वाला फाइलों को इस्तेमाल किया है. आपको यह मसवरा दिया जा ता है कि आप स्थापना को यह अनुप्रयोगौं को खुद ही बन्द करने कि इजाजत प्रदान करें. अधिष्ठापन खतम होने के वाद, स्थापना यह अनुप्रयोग कों फिर सुरु करने कि कोसिस करेगा.
CloseApplications=&खुद हि अनुप्रयोग कों बन्द करें
DontCloseApplications=अनुप्रयोग कों बन्द &नहि करें
ErrorCloseApplications=स्थापना खुद हि सभी अनुप्रयोगों को बन्द नहि कर सका. आपको यह मसवरा दिया जाता है कि स्थापना ने अपडेट करने वाली फाइलों को इस्तमाल कर रहे अनुप्रयोगौं को आगे बढ्ने से पहले आप खुद ही बन्द करें.
; "Installing" wizard page
WizardInstalling=अधिष्ठापन हो रहा है.
InstallingLabel=जब तक स्थापना आपके कल्पयन्त्र में [name] अधिष्ठापन करता है, उस वख्त तक कृपया प्रतीक्षा करे.
; "Setup Completed" wizard page
FinishedHeadingLabel=[name] स्थापना कि कार्य पूरा हो रहा है.
FinishedLabelNoIcons=स्थापना ने [name] को आपके कल्पयन्त्र में सफलतापूर्वक अधिष्ठापन कर दिया है.
FinishedLabel=स्थापना ने [name] आपके कल्पयन्त्रमें अधिष्ठापन कर दिया है. आप उपयुक्त प्रतिमा पे क्लिक कर के कभी भी ये कार्यक्रम शुरू कर सकते है.
ClickFinish=स्थापना से बाहर निकलने वास्ते समाप्त पे क्लिक करे.
FinishedRestartLabel=[name] का अधिष्ठापन पूरा करने वास्ते कल्पयन्त्र फिर सुरु करना बेहद जरूरी है. %n%nक्या आप अभी रिसुरु करना चाहते है?
FinishedRestartMessage=[name] का अधिष्ठापन पूरा करने हेतु कल्पयन्त्र फिर सुरु करना बेहद जरूरी है.%n%nक्या आप अभी फिर सुरु करना चाहते है?
ShowReadmeCheck=हाँ मुझे हमें पढो file देखनी है.
YesRadio=&हाँ, कल्पयन्त्र फिर सुरु कर दो.
NoRadio=&नही मै अपना कल्पयन्त्र स्वयं बाद में फिर सुरु करूँगा.
; used for example as 'Run MyProg.exe'
RunEntryExec=रन %1
; used for example as 'View Readme.txt'
RunEntryShellExec=देखे %1
; "Setup Needs the Next Disk" stuff
ChangeDiskTitle=स्थापना के लिए अगली डिस्क चाहिए.
SelectDiskLabel2=कृपया डिस्क %1 डालके ठीक दबाए.%n%nयदि इस डिस्क की फाइल नही मिलती तो सही पथ बताए या ब्राउज़ पे क्लिक करे.
PathLabel=पथ:
FileNotInDir2=फाइल "%1" को "%2" में ढुढ नही पाए. कृपया सही डिस्क डाले या अलग फोल्डर चयन करे.
SelectDirectoryLabel=अगली डिस्क का पता बताए.
; *** Installation phase messages
SetupAborted=स्थापना पूरा नही हो पाया.%n%nकृपया त्रुटी ठीक करे और फिर से प्रयास करे.
EntryAbortRetryIgnore=फिर से प्रयास करने वास्ते Retry दबाए, यदि ऐसे ही आगे बढ़ना है तो Ignore दबाए, या तो स्थापना रद्द करेँ करने वास्ते Abort दबाए.
; *** Installation status messages
StatusClosingApplications=अनुप्रयोगकों बन्द किया जा रहा है.
StatusCreateDirs=सङ्ग्रहिका बना रहा है...
StatusExtractFiles=फाइल उत्खनन कर रहा है...
StatusCreateIcons=छोटीरास्ता बना रहा है...
StatusCreateIniEntries=INI एंट्री बना रहा है...
StatusCreateRegistryEntries=पञ्जीका एंट्री बना रहा है...
StatusRegisterFiles=फाइल पञ्जिकृत कर रहा है...
StatusSavingUninstall=निस्कासन की सुचनाए बचतकर रहा है...
StatusRunProgram=अधिष्ठापन पूरा कर रहा है...
StatusRestartingApplications=अनुप्रयोगकों कि फिर सुरुवात
StatusRollback=बदलावों को पिछे हट्ने कि काम कर रहा है...
; *** Misc. errors
ErrorInternal2=आंतरिक त्रुटी: %1
ErrorFunctionFailedNoCode=%1 विफल
ErrorFunctionFailed=%1 विफल; कोड %2
ErrorFunctionFailedWithMessage=%1 विफल; कोड %2.%n%3
ErrorExecutingProgram=फाइल को कार्यान्वयन नही कर पा रहा:%n%1
; *** Registry errors
ErrorRegOpenKey=पञ्जीका कुञ्जी खोलते वक्त त्रुटी:%n%1\%2
ErrorRegCreateKey=पञ्जीका कुञ्जी बनाते वक्त त्रुटी:%n%1\%2
ErrorRegWriteKey=पञ्जीका कुञ्जी में लिखते वक्त त्रुटी:%n%1\%2
; *** INI errors
ErrorIniEntry=फ़ाइल "%1" में INI एंट्री डालते वक्त त्रुटी.
; *** File copying errors
FileAbortRetryIgnore=फिर से प्रयास करने हेतु Retry बटन दबाए, यदि ऐसे ही आगे बढ़ना है तो Ignore दबाए(हम ऐसा सुजाव नही देते),या तो Abort दबाए अधिष्ठापन को रद्द करेँ करने हेतु.
FileAbortRetryIgnore2=फिर से प्रयास करने हेतु Retry बटन दबाए, यदि ऐसे ही आगे बढ़ना है तो Ignore दबाए(हम ऐसा सुजाव नही देते),या तो Abort दबाए अधिष्ठापन को रद्द करेँ करने हेतु.
SourceIsCorrupted=श्रोत फ़ाइल में गडबड है.
SourceDoesntExist=श्रोत फाइल "%1" मौजूद ही नही है.
ExistingFileReadOnly=मौजदा फ़ाइल सिर्फ-पढो है.%n%n आप Retry पे क्लिक करे, उसका सिर्फ-पढो attribute हटाने के लिए और फिर दोबारा प्रयास करे. यदि इस फाइल को छोड़ देना है तो Ignore, और यदि अधिष्ठापन रद्द करेँ करना है तो Abort बटन दबाए.
ErrorReadingExistingDest=मौजदा फाइल को पढते वक्त त्रुटी:
FileExists=फाइल पहेले से मौजूद है.%n%nक्या आप उसको ओवर-राईट करना चाहते हो?
ExistingFileNewer=मौजूदा फाइल स्थापना फ़ाइल से नई है. हमारा सुजाव है की आप इसे रखे.%n%nक्या आप फ़ाइल को रखना चाहते है?
ErrorChangingAttr=मौजूदा फाइल के एट्रीब्यूट बदलते वक्त त्रुटी:
ErrorCreatingTemp=फ़ाइल बनाते वख्त त्रुटी:
ErrorReadingSource=श्रोत फाइल खोलते वक्त त्रुटी:
ErrorCopying=फ़ाइल प्रति करने का प्रयास करते वक्त त्रुटी:
ErrorReplacingExistingFile=मौजूद फाइल को प्रतिस्थापना करते वक्त त्रुटी:
ErrorRestartReplace=प्रतिस्थापन कि फिर से सुरुवात विफल रहा:
ErrorRenamingTemp=सङ्ग्रहिका में फाइल का नाम बदलते वक्त त्रुटी हुई:
ErrorRegisterServer=इस को पञ्जिकृत नही कर पा रहा DLL/OCX: %1
ErrorRegSvr32Failed=RegSvr32 असफल हो गयी, बाहर जाने कोड %1 के साथ
ErrorRegisterTypeLib=इस टाइप लाइब्रेरी को पंजीकृत नही कर पा रहा: %1
; *** Post-installation errors
ErrorOpeningReadme=मुझे पढो फ़ाइल खोलते वक्त त्रुटी हुई.
ErrorRestartingComputer=स्थापना कल्पयन्त्र को फिर सुरु करने में असफल रहा. कृपया आप ही इसको फिर सुरु करे.
; *** Uninstaller messages
UninstallNotFound=फाइल "%1" मौजूद ही नही है. निस्कासन करना असंभव.
UninstallOpenError=फ़ाइल "%1" खुल नही रही. निस्कासन करना असंभव.
UninstallUnsupportedVer=निस्कासन लोग फ़ाइल "%1" जिस फोर्मेट में है उसे हम पहचान नही पा रहे. आगे बढ़ना नामुमकिन.
UninstallUnknownEntry=निस्कासन लोग में एक अज्ञात प्रविष्टी (%1)मिली.
ConfirmUninstall=क्या पक्का आप %1 को निस्कासन करना चाहते हो?
UninstallOnlyOnWin64=केवल 64-bit Windows से ही इसे निस्कासन किया जा सकता है.
OnlyAdminCanUninstall=केवल प्रशासक खातों से ही इसे निस्कासन किया जा सकता है..
UninstallStatusLabel=जब तक %1 नही हड्ता, धैर्य रखे.
UninstalledAll=%1 सफलतापूर्वक निस्कासन हुआ.
UninstalledMost=%1 निस्कासन पूरा हुआ.%n%nकुछ तत्वों को निकाल नही पाए लेकिन आप उन्हें अपनि तरह से हटा सकते हो.
UninstalledAndNeedsRestart=%1 का निस्कासन पूरा करने वास्ते कल्पयन्त्र को फिर सुरु करना जरूरी है.%n%nक्या अभी फिर सुरु करे?
UninstallDataCorrupted=%1 फ़ाइल में त्रुटी. निस्कासन नामुमकिन.
; *** Uninstallation phase messages
ConfirmDeleteSharedFileTitle=क्या शेरेड-फाइल को निकाल देना है?
ConfirmDeleteSharedFile2=प्रणाली से ये ज्ञात होता है की निम्नलिखिती शेरेड-फ़ाइल अब आगे इस्तेमाल में नही आएगी. क्या आप उन्हें भी निस्कासन करना चाहते है?%n%n यदि कोई अन्य कार्यक्रम इन फाइल पे आधारित है तो वो शायद इन्हें निकाल देने पर ढंग से काम ना भी करे. यदि आप फैसला नही कर पा रहे तो 'नही' पे क्लिक करे. इन फाइल को कल्पयन्त्र में पड़े रहेने दोगे तो भी कोई नुकसान नही होगा.
SharedFileNameLabel=फाइल नाम:
SharedFileLocationLabel=पता:
WizardUninstalling=निस्कासन स्थिति
StatusUninstalling=निस्कासन हो रहा है %1...
; *** Shutdown block reasons
ShutdownBlockReasonInstallingApp= %1 कि अधिष्ठआपन हो रही है.
ShutdownBlockReasonUninstallingApp=%1 कि निस्कासन हो रही है.
; The custom messages below aren't used by Setup itself, but if you make
; use of them in your scripts, you'll want to translate them.
[CustomMessages]
NameAndVersion=%1 संस्करण %2
AdditionalIcons=अतिरिक्त प्रतिमा:
CreateDesktopIcon=डेस्कटॉप प्रतिमा बनाए
CreateQuickLaunchIcon=जल्दि चलो प्रतिमा बनाए
ProgramOnTheWeb=%1 इन्टरनेट पे
UninstallProgram=निस्कासन करे %1
LaunchProgram=लोंच करे %1
AssocFileExtension=%1 को %2 फ़ाइल एक्सटेंशन के साथ आबद्ध करे
AssocingFileExtension=%1 को %2 फ़ाइल एक्सटेंशन के साथ आबद्ध कर रहा है....
AutoStartProgramGroupDescription=सुरुवात
AutoStartProgram=%1 को %2 फ़ाइल एक्सटेंशन के साथ आबद्ध कर रहा है....
AddonHostProgramNotFound=आपने चयन किया हुआफोल्डर में %1 नही मिला. %n%nक्या आप किसि हालत में यस कि निरन्तरता रख्ना चाहते है ?
@@ -0,0 +1,350 @@
; *** Inno Setup version 6.4.0+ Indonesian messages ***
;
; Untuk mengunduh terjemahan kontribusi-pengguna dari berkas ini, buka:
; http://www.jrsoftware.org/files/istrans/
;
; Alih bahasa oleh: MozaikTM (mozaik.tm@gmail.com)
[LangOptions]
LanguageName=Bahasa Indonesia
LanguageID=$0421
LanguageCodePage=0
[Messages]
SetupAppTitle=Instalasi
SetupWindowTitle=Instalasi - %1
UninstallAppTitle=Pelepas
UninstallAppFullTitle=Pelepasan %1
InformationTitle=Informasi
ConfirmTitle=Konfirmasi
ErrorTitle=Galat
SetupLdrStartupMessage=Kami akan memasang %1. Teruskan?
LdrCannotCreateTemp=Tidak dapat membuat berkas sementara. Batal memasang
LdrCannotExecTemp=Tidak dapat menjalankan berkas di direktori sementara. Batal memasang
LastErrorMessage=%1.%n%nGalat %2: %3
SetupFileMissing=Berkas %1 hilang dari direktori instalasi. Silakan koreksi masalah atau dapatkan salinan program yang baru.
SetupFileCorrupt=Berkas pemandu telah rusak. Silakan dapatkan salinan program yang baru.
SetupFileCorruptOrWrongVer=Berkas pemandu telah rusak, atau tidak cocok dengan versi pemandu ini. Silakan koreksi masalah atau dapatkan salinan program yang baru.
InvalidParameter=Parameter tak sah terdapat pada baris perintah: %n%n%1
SetupAlreadyRunning=Pemandu sudah berjalan.
WindowsVersionNotSupported=Program ini tidak mendukung versi Windows yang berjalan pada komputer Anda.
WindowsServicePackRequired=Program ini memerlukan %1 Service Pack %2 atau yang terbaru.
NotOnThisPlatform=Program ini tidak akan berjalan pada %1.
OnlyOnThisPlatform=Program ini harus dijalankan pada %1.
OnlyOnTheseArchitectures=Program ini hanya bisa dipasang pada versi Windows yang didesain untuk arsitektur prosesor berikut:%n%n%1
WinVersionTooLowError=Program ini memerlukan %1 versi %2 atau yang terbaru.
WinVersionTooHighError=Program ini tidak dapat dipasang pada %1 versi %2 atau yang terbaru.
AdminPrivilegesRequired=Anda harus masuk sebagai seorang administrator saat memasang program ini.
PowerUserPrivilegesRequired=Anda harus masuk sebagai seorang administrator atau anggota grup Power Users saat memasang program ini.
SetupAppRunningError=Kami mendeteksi bahwa %1 sedang berjalan.%n%nSilakan tutup semua instansi bersangkutan, lalu klik OK untuk meneruskan, atau Cancel untuk keluar.
UninstallAppRunningError=Pelepas mendeteksi bahwa %1 sedang berjalan.%n%nSilakan tutup semua instansi bersangkutan, lalu klik OK untuk meneruskan, atau Cancel untuk keluar.
;Inno6
PrivilegesRequiredOverrideTitle=Pilih Mode Instalasi
PrivilegesRequiredOverrideInstruction=Pilih mode instalasi
PrivilegesRequiredOverrideText1=%1 bisa dipasang untuk semua pengguna (perlu izin administratif), atau hanya Anda.
PrivilegesRequiredOverrideText2=%1 bisa dipasang hanya untuk Anda, atau semua pengguna (perlu izin administratif).
PrivilegesRequiredOverrideAllUsers=Pasang untuk &semua pengguna
PrivilegesRequiredOverrideAllUsersRecommended=Pasang untuk &semua pengguna (disarankan)
PrivilegesRequiredOverrideCurrentUser=Pasang &hanya untuk saya
PrivilegesRequiredOverrideCurrentUserRecommended=Pasang &hanya untuk saya (disarankan)
;Inno6
ErrorCreatingDir=Kami tidak dapat membuat direktori "%1"
ErrorTooManyFilesInDir=Tidak dapat membuat berkas di direktori "%1" karena berisi terlalu banyak berkas
ExitSetupTitle=Keluar Pemandu
ExitSetupMessage=Instalasi tidak lengkap. Bila Anda keluar sekarang, program takkan terpasang.%n%nAnda bisa menjalankan Pemandu lagi lain kali untuk melengkapinya.%n%nKeluar?
AboutSetupMenuItem=&Tentang Pemandu...
AboutSetupTitle=Tentang Pemandu
AboutSetupMessage=%1 versi %2%n%3%n%n%1 laman beranda:%n%4
AboutSetupNote=
TranslatorNote=
ButtonBack=&Kembali
ButtonNext=&Maju
ButtonInstall=&Pasang
ButtonOK=Oke
ButtonCancel=Batal
ButtonYes=&Ya
ButtonYesToAll=Y&a semuanya
ButtonNo=&Tidak
ButtonNoToAll=T&idak semuanya
ButtonFinish=&Selesai
ButtonBrowse=&Cari...
ButtonWizardBrowse=C&ari...
ButtonNewFolder=&Buat Map Baru
SelectLanguageTitle=Pilih Bahasa Pemandu
SelectLanguageLabel=Pilih bahasa untuk digunakan ketika memasang.
ClickNext=Klik Maju untuk meneruskan, atau Batal untuk keluar.
BeveledLabel=
BrowseDialogTitle=Cari Map
BrowseDialogLabel=Pilih map dari daftar berikut, lalu klik OK.
NewFolderName=Map Baru
WelcomeLabel1=Selamat datang di Pemandu Instalasi [name]
WelcomeLabel2=Kami akan memasang [name/ver] pada komputer Anda.%n%nDisarankan untuk menutup semua aplikasi lainnya sebelum meneruskan.
WizardPassword=Kata Sandi
PasswordLabel1=Instalasi ini dilindungi kata sandi.
PasswordLabel3=Silakan masukkan kata sandi, lalu klik Maju untuk meneruskan. Kata sandi bersifat sensitif-kapitalisasi.
PasswordEditLabel=&Kata Sandi:
IncorrectPassword=Kata sandi yang Anda masukkan salah. Silakan coba lagi.
WizardLicense=Kesepakatan Lisensi
LicenseLabel=Silakan baca informasi berikut sebelum meneruskan.
LicenseLabel3=Silakan baca Kesepakatan Lisensi berikut. Anda harus setuju dengan syarat dari kesepakatan ini sebelum meneruskan instalasi.
LicenseAccepted=Saya &setujui kesepakatan ini
LicenseNotAccepted=Saya &tidak setuju kesepakatan ini
WizardInfoBefore=Informasi
InfoBeforeLabel=Silakan baca informasi penting berikut sebelum meneruskan.
InfoBeforeClickLabel=Saat Anda siap meneruskan instalasi, klik Maju.
WizardInfoAfter=Informasi
InfoAfterLabel=Silakan baca informasi penting berikut sebelum meneruskan.
InfoAfterClickLabel=Saat Anda siap meneruskan instalasi, klik Maju.
WizardUserInfo=Informasi Pengguna
UserInfoDesc=Silakan masukkan informasi Anda.
UserInfoName=&Nama Pengguna:
UserInfoOrg=&Organisasi:
UserInfoSerial=&Nomor Seri:
UserInfoNameRequired=Wajib memasukkan nama.
WizardSelectDir=Pilih Lokasi Tujuan
SelectDirDesc=Di manakah [name] sebaiknya dipasang?
SelectDirLabel3=Kami akan memasang [name] ke dalam map berikut.
SelectDirBrowseLabel=Untuk meneruskan, klik Maju. Bila Anda ingin memilih map lain, klik Cari.
;Inno6
DiskSpaceGBLabel=Diperlukan sedikitnya [gb] GB ruang bebas.
;Inno6
DiskSpaceMBLabel=Diperlukan sedikitnya [mb] MB ruang bebas.
CannotInstallToNetworkDrive=Kami tidak bisa memasang ke diska jaringan.
CannotInstallToUNCPath=Kami tidak bisa memasang ke alamat UNC.
InvalidPath=Anda wajib memasukkan alamat lengkap dengan huruf diska; contohnya:%n%nC:\APP%n%natau alamat UNC dalam bentuk:%n%n\\server\share
InvalidDrive=Diska atau alamat UNC yang Anda pilih tidak ada atau tidak dapat diakses. Silakan pilih yang lain.
DiskSpaceWarningTitle=Ruang Bebas Tidak Cukup
DiskSpaceWarning=Kami memerlukan sedikitnya %1 KB ruang bebas untuk memasang, namun diska yang Anda pilih hanya memiliki %2 KB tersedia.%n%nMaju terus?
DirNameTooLong=Nama map atau alamat terlalu panjang.
InvalidDirName=Nama map tidak sah.
BadDirName32=Nama map dilarang berisi karakter-karakter berikut:%n%n%1
DirExistsTitle=Map Sudah Ada
DirExists=Map:%n%n%1%n%nsudah ada. Tetap pasang di map tersebut?
DirDoesntExistTitle=Map Tidak Ada
DirDoesntExist=Map:%n%n%1%n%ntidak ada. Buat map?
WizardSelectComponents=Pilih Komponen
SelectComponentsDesc=Komponen mana sajakah yang sebaiknya dipasang?
SelectComponentsLabel2=Centang komponen yang Anda inginkan; hapus centang dari komponen yang tidak Anda inginkan. Klik Maju saat Anda siap meneruskan.
FullInstallation=Instalasi penuh
CompactInstallation=Instalasi padat
CustomInstallation=Instalasi kustom
NoUninstallWarningTitle=Komponen Terpasang
NoUninstallWarning=Kami mendeteksi bahwa komponen berikut telah terpasang pada komputer Anda:%n%n%1%n%nMembatalkan pilihan atas komponen berikut bukan berarti melepasnya.%n%nMaju terus?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
;Inno6
ComponentsDiskSpaceGBLabel=Pilihan saat ini memerlukan sedikitnya [gb] GB ruang bebas.
;Inno6
ComponentsDiskSpaceMBLabel=Pilihan saat ini memerlukan sedikitnya [mb] MB ruang bebas.
WizardSelectTasks=Pilih Tugas Tambahan
SelectTasksDesc=Tugas tambahan mana sajakah yang sebaiknya dijalankan?
SelectTasksLabel2=Pilih tugas tambahan yang Anda ingin kami jalankan ketika memasang [name], lalu klik Maju.
WizardSelectProgramGroup=Pilih Map Menu Start
SelectStartMenuFolderDesc=Di manakah sebaiknya kami letakkan pintasan program?
SelectStartMenuFolderLabel3=Kami akan membuat pintasan program di map Menu Start berikut.
SelectStartMenuFolderBrowseLabel=Untuk meneruskan, klik Maju. Bila Anda ingin memilih map lain, klik Cari.
MustEnterGroupName=Anda wajib memasukkan nama map.
GroupNameTooLong=Nama map atau alamat terlalu panjang.
InvalidGroupName=Nama map tidak sah.
BadGroupName=Nama map dilarang berisi karakter-karakter berikut:%n%n%1
NoProgramGroupCheck2=&Jangan buat map Menu Start
WizardReady=Siap Memasang
ReadyLabel1=Kami siap untuk memulai instalasi [name] pada komputer Anda.
ReadyLabel2a=Klik Pasang untuk meneruskan instalasi, atau klik Kembali bila Anda ingin menilik atau mengubah setelan.
ReadyLabel2b=Klik Pasang untuk meneruskan instalasi.
ReadyMemoUserInfo=Informasi pengguna:
ReadyMemoDir=Lokasi tujuan:
ReadyMemoType=Tipe instalasi:
ReadyMemoComponents=Komponen terpilih:
ReadyMemoGroup=Map Menu Start:
ReadyMemoTasks=Tugas Tambahan:
;Inno6
DownloadingLabel=Mengunduh berkas tambahan...
ButtonStopDownload=&Setop Unduhan
StopDownload=Anda yakin ingin berhenti mengunduh?
ErrorDownloadAborted=Unduhan dibatalkan
ErrorDownloadFailed=Gagal mengunduh: %1 %2
ErrorDownloadSizeFailed=Gagal mendapatkan ukuran: %1 %2
ErrorFileHash1=Ceksum berkas gagal: %1
ErrorFileHash2=Ceksum berkas tidak sah: seharusnya %1, yang kami dapatkan %2
ErrorProgress=Langkah tidak sah: %1 dari %2
ErrorFileSize=Ukuran berkas tidak sah: seharusnya %1, yang kami dapatkan %2
; *** TExtractionWizardPage wizard page and Extract7ZipArchive
ExtractionLabel=Mengektrasi berkas tambahan...
ButtonStopExtraction=&Hentikan ekstrasi
StopExtraction=Anda yakin ingin menghentikan ekstrasi?
ErrorExtractionAborted=Ekstrasi dibatalkan
ErrorExtractionFailed=Ekstraksi gagal: %1
;Inno6
WizardPreparing=Bersiap Memasang
PreparingDesc=Kami sedang bersiap memasang [name] pada komputer Anda.
PreviousInstallNotCompleted=Instalasi/pelepasan dari program sebelumnya tidak lengkap. Anda perlu memulai ulang komputer untuk melengkapinya nanti.%n%nSetelah itu, jalankan Pemandu kembali untuk melengkapi instalasi [name].
CannotContinue=Kami tidak bisa meneruskan. Klik Batal untuk keluar.
ApplicationsFound=Aplikasi berikut tengah memakai berkas-berkas yang perlu kami perbarui. Disarankan agar Anda mengizinkan kami untuk menutupnya secara otomatis.
ApplicationsFound2=Aplikasi berikut tengah memakai berkas-berkas yang perlu kami perbarui. Disarankan agar Anda mengizinkan kami untuk menutupnya secara otomatis. Selengkapnya memasang, kami akan berusaha memulai ulang aplikasi-aplikasi tersebut.
CloseApplications=&Otomatis tutup aplikasi
DontCloseApplications=&Jangan tutup aplikasi
ErrorCloseApplications=Kami tidak dapat menutup semua aplikasi secara otomatis. Disarankan agar Anda menutup semua aplikasi yang memakai berkas-berkas yang perlu kami perbarui sebelum meneruskan.
;Inno6
PrepareToInstallNeedsRestart=Kami perlu memulai ulang komputer Anda. Setelah itu, jalankan Pemandu kembali untuk melengkapi pemasangan [name].%n%nMulai ulang sekarang?
;Inno6
WizardInstalling=Memasang
InstallingLabel=Silakan tunggu selagi kami memasang [name] pada komputer Anda.
FinishedHeadingLabel=Mengakhiri Instalasi [name]
FinishedLabelNoIcons=Kami telah selesai memasang [name] pada komputer Anda.
FinishedLabel=Kami telah selesai memasang [name] pada komputer Anda. Aplikasi tersebut bisa dijalankan dengan cara memilih pintasan yang terpasang.
ClickFinish=Klik Selesai untuk menutup instalasi.
FinishedRestartLabel=Demi melengkapi instalasi [name], kami perlu memulai ulang komputer Anda. Lakukan sekarang?
FinishedRestartMessage=Demi melengkapi instalasi [name], kami perlu memulai ulang komputer Anda.%n%nLakukan sekarang?
ShowReadmeCheck=Ya, saya ingin melihat berkas README
YesRadio=&Ya, mulai ulang komputer sekarang
NoRadio=&Tidak, saya akan memulai ulang komputer nanti
RunEntryExec=Jalankan %1
RunEntryShellExec=Lihat %1
ChangeDiskTitle=Kami Memerlukan Diska Sambungan
SelectDiskLabel2=Silakan masukkan Diska %1 dan klik OK.%n%nBila berkas-berkas di dalam diska ini dapat ditemukan di map lain selain yang ditampilkan di bawah, masukkan alamat yang benar atau klik Cari.
PathLabel=&Alamat:
FileNotInDir2=Berkas "%1" tidak dapat ditemukan di "%2". Silakan masukkan diska yang benar atau pilih map lain.
SelectDirectoryLabel=Silakan tentukan lokasi diska berikutnya.
SetupAborted=Instalasi tidak lengkap.%n%nSilakan koreksi masalah dan jalankan Pemandu kembali.
;Inno6
AbortRetryIgnoreSelectAction=Pilih tindakan
AbortRetryIgnoreRetry=&Coba lagi
AbortRetryIgnoreIgnore=&Abaikan galat dan teruskan
AbortRetryIgnoreCancel=Batalkan pemasangan
;Inno6
StatusClosingApplications=Menutup aplikasi...
StatusCreateDirs=Membuat direktori...
StatusExtractFiles=Mengekstrak berkas...
StatusCreateIcons=Membuat pintasan...
StatusCreateIniEntries=Membuat catatan INI...
StatusCreateRegistryEntries=Membuat catatan Registry...
StatusRegisterFiles=Meregistrasi berkas...
StatusSavingUninstall=Menyimpan informasi pelepas...
StatusRunProgram=Mengakhiri instalasi...
StatusRestartingApplications=Menjalankan ulang aplikasi...
StatusRollback=Membatalkan perubahan...
ErrorInternal2=Galat internal: %1
ErrorFunctionFailedNoCode=%1 gagal
ErrorFunctionFailed=%1 gagal; kode %2
ErrorFunctionFailedWithMessage=%1 gagal; kode %2.%n%3
ErrorExecutingProgram=Tidak dapat mengeksekusi berkas:%n%1
ErrorRegOpenKey=Galat membuka kunci Registry:%n%1\%2
ErrorRegCreateKey=Galat membuat kunci Registry:%n%1\%2
ErrorRegWriteKey=Galat menulis kunci Registry:%n%1\%2
ErrorIniEntry=Galat membuat catatan INI dalam berkas "%1".
;Inno6
FileAbortRetryIgnoreSkipNotRecommended=&Lewati berkas ini (tidak disarankan)
FileAbortRetryIgnoreIgnoreNotRecommended=&Abaikan galat dan teruskan (tidak disarankan)
;Inno6
SourceIsCorrupted=Berkas asal telah rusak
SourceDoesntExist=Berkas asal "%1" tidak ada
;Inno6
ExistingFileReadOnly2=Berkas yang sudah ada tidak bisa ditimpa karena telah ditandai hanya-baca.
ExistingFileReadOnlyRetry=&Hapus atribut hanya-baca dan coba lagi
ExistingFileReadOnlyKeepExisting=&Pertahankan berkas yang sudah ada
ErrorReadingExistingDest=Terjadi galat saat berusaha membaca berkas yang sudah ada:
FileExistsSelectAction=Pilih tindakan
FileExists2=Berkas sudah ada.
FileExistsOverwriteExisting=&Timpa berkas yang sudah ada
FileExistsKeepExisting=&Pertahankan berkas yang sudah ada
FileExistsOverwriteOrKeepAll=&Lakukan ini untuk konflik (bentrok) berikutnya
ExistingFileNewerSelectAction=Pilih tindakan
ExistingFileNewer2=Berkas yang sudah ada lebih baru dari yang akan kami coba pasang.
ExistingFileNewerOverwriteExisting=&Timpa berkas yang sudah ada
ExistingFileNewerKeepExisting=&Pertahankan berkas yang sudah ada (disarankan)
ExistingFileNewerOverwriteOrKeepAll=&Lakukan ini untuk konflik (bentrok) berikutnya
;Inno6
ErrorReadingExistingDest=Terjadi galat saat berusaha membaca berkas yang sudah ada:
ErrorChangingAttr=Terjadi galat saat berusaha mengubah atribusi berkas yang sudah ada:
ErrorCreatingTemp=Terjadi galat saat berusaha membuat berkas di direktori tujuan:
ErrorReadingSource=Terjadi galat saat berusaha membaca berkas asal:
ErrorCopying=Terjadi galat saat berusaha menyalin berkas:
ErrorReplacingExistingFile=Terjadi galat saat berusaha menimpa berkas yang sudah ada:
ErrorRestartReplace=RestartReplace gagal:
ErrorRenamingTemp=Terjadi galat saat berusaha mengubah nama berkas di direktori tujuan:
ErrorRegisterServer=Tidak dapat meregistrasi DLL/OCX: %1
ErrorRegSvr32Failed=RegSvr32 gagal dengan kode akhir %1
ErrorRegisterTypeLib=Tidak dapat meregistrasi berkas referensi: %1
;Inno6
UninstallDisplayNameMark=%1 (%2)
UninstallDisplayNameMarks=%1 (%2, %3)
UninstallDisplayNameMark32Bit=32-bita
UninstallDisplayNameMark64Bit=64-bita
UninstallDisplayNameMarkAllUsers=Semua pengguna
UninstallDisplayNameMarkCurrentUser=Pengguna saat ini
;Inno6
ErrorOpeningReadme=Terjadi galat saat berusaha membuka berkas README.
ErrorRestartingComputer=Kami gagal memulai ulang komputer. Silakan lakukan secara manual.
UninstallNotFound=Berkas "%1" tidak ada. Tidak bisa melepas
UninstallOpenError=Berkas "%1" tidak dapat dibuka. Tidak bisa melepas
UninstallUnsupportedVer=Berkas catatan pelepas "%1" tidak dalam format yang kami kenali. Tidak bisa melepas
UninstallUnknownEntry=Entri tak dikenal (%1) ditemukan dalam catatan pelepas
ConfirmUninstall=Anda yakin ingin melepas %1 beserta semua komponennya?
UninstallOnlyOnWin64=Instalasi ini hanya bisa dilepas pada Windows 64-bita.
OnlyAdminCanUninstall=Instalasi ini hanya bisa dilepas oleh pengguna dengan izin administratif.
UninstallStatusLabel=Silakan tunggu selagi %1 dihapus dari komputer Anda.
UninstalledAll=%1 berhasil dihapus dari komputer Anda.
UninstalledMost=Selesai melepas %1.%n%nBeberapa elemen tidak dapat dihapus. Anda bisa menghapusnya secara manual.
UninstalledAndNeedsRestart=Untuk melengkapi pelepasan %1, komputer Anda perlu dimulai ulang.%n%nMulai ulang sekarang?
UninstallDataCorrupted=Berkas "%1" rusak. Tidak bisa melepas
ConfirmDeleteSharedFileTitle=Hapus Berkas Bersama?
ConfirmDeleteSharedFile2=Sistem mengindikasi bahwa berkas bersama di bawah ini tidak lagi dipakai oleh program mana pun. Apa Anda ingin agar kami menghapusnya?%n%nBila masih ada program yang memakainya dan berkas ini dihapus, program tersebut dapat tidak berfungsi dengan semestinya. Bila Anda ragu, pilih No. Membiarkan berkas ini pada sistem Anda takkan membahayakan.
SharedFileNameLabel=Nama berkas:
SharedFileLocationLabel=Lokasi:
WizardUninstalling=Status Pelepasan
StatusUninstalling=Melepas %1...
ShutdownBlockReasonInstallingApp=Memasang %1.
ShutdownBlockReasonUninstallingApp=Melepas %1.
[CustomMessages]
NameAndVersion=%1 versi %2
AdditionalIcons=Pintasan tambahan:
CreateDesktopIcon=Buat pintasan &desktop
CreateQuickLaunchIcon=Buat pintasan Pelontar &Cepat
ProgramOnTheWeb=%1 di Web
UninstallProgram=Lepas %1
LaunchProgram=Jalankan %1
AssocFileExtension=&Kaitkan %1 dengan ekstensi berkas %2
AssocingFileExtension=Mengaitkan %1 dengan ekstensi berkas %2...
AutoStartProgramGroupDescription=Startup:
AutoStartProgram=Otomatis jalankan %1
AddonHostProgramNotFound=%1 tidak dapat ditemukan di map yang Anda pilih.%n%nMaju terus?
+157
View File
@@ -0,0 +1,157 @@
; Script generated by the Inno Setup Script Wizard.
#define MyAppName "YTSage"
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#define MyAppPublisher "oop7"
#define MyAppURL "https://github.com/oop7/YTSage/"
#ifndef MyAppExeName
#define MyAppExeName "YTSage-ffmpeg.exe"
#endif
#ifndef SourceDir
#define SourceDir "..\dist\YTSage-FFmpeg"
#endif
[Setup]
AppId={{56997322-2A3A-4338-AEF1-C3C8BB28AC4F}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
LicenseFile=..\LICENSE
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputBaseFilename=YTSage-v{#MyAppVersion}-ffmpeg-Setup
SolidCompression=yes
WizardStyle=modern
OutputDir=..\artifacts
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "arabic"; MessagesFile: "compiler:Languages\Arabic.isl"
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
Name: "chinesesimplified"; MessagesFile: "Languages\Unofficial\ChineseSimplified.isl"
Name: "hindi"; MessagesFile: "Languages\Unofficial\Hindi.islu"
Name: "indonesian"; MessagesFile: "Languages\Unofficial\Indonesian.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
Name: "addtopath"; Description: "Add ffmpeg to user PATH environment variable"; GroupDescription: "Additional options:"; Flags: unchecked
[Files]
Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Registry]
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath('{app}')
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Code]
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
ParamExpanded: string;
begin
// Expand the setup constants like {app} from Param
ParamExpanded := ExpandConstant(Param);
if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then
begin
Result := True;
exit;
end;
// Look for the path with leading and trailing semicolon
Result := Pos(';' + UpperCase(ParamExpanded) + ';', ';' + UpperCase(OrigPath) + ';') = 0;
if Result = True then
Result := Pos(';' + UpperCase(ParamExpanded) + '\;', ';' + UpperCase(OrigPath) + ';') = 0;
end;
procedure UpdateLanguageConfig;
var
LanguageCode: string;
ConfigPath: string;
FileContent: AnsiString;
JsonContent: string;
LangPattern: string;
P_Start, P_End: Integer;
begin
if ActiveLanguage = 'english' then LanguageCode := 'en'
else if ActiveLanguage = 'arabic' then LanguageCode := 'ar'
else if ActiveLanguage = 'brazilianportuguese' then LanguageCode := 'pt-br'
else if ActiveLanguage = 'french' then LanguageCode := 'fr'
else if ActiveLanguage = 'german' then LanguageCode := 'de'
else if ActiveLanguage = 'italian' then LanguageCode := 'it'
else if ActiveLanguage = 'japanese' then LanguageCode := 'ja'
else if ActiveLanguage = 'polish' then LanguageCode := 'pl'
else if ActiveLanguage = 'portuguese' then LanguageCode := 'pt'
else if ActiveLanguage = 'russian' then LanguageCode := 'ru'
else if ActiveLanguage = 'spanish' then LanguageCode := 'es'
else if ActiveLanguage = 'turkish' then LanguageCode := 'tr'
else if ActiveLanguage = 'chinesesimplified' then LanguageCode := 'zh'
else if ActiveLanguage = 'hindi' then LanguageCode := 'hi'
else if ActiveLanguage = 'indonesian' then LanguageCode := 'id'
else LanguageCode := 'en';
ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json');
if not DirExists(ExtractFilePath(ConfigPath)) then
ForceDirectories(ExtractFilePath(ConfigPath));
if FileExists(ConfigPath) then
begin
if LoadStringFromFile(ConfigPath, FileContent) then
begin
JsonContent := String(FileContent);
LangPattern := '"language": "';
P_Start := Pos(LangPattern, JsonContent);
if P_Start > 0 then
begin
P_Start := P_Start + Length(LangPattern);
P_End := Pos('"', Copy(JsonContent, P_Start, Length(JsonContent)));
if P_End > 0 then
begin
Delete(JsonContent, P_Start, P_End - 1);
Insert(LanguageCode, JsonContent, P_Start);
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
end;
end
else
begin
JsonContent := '{' + #13#10 +
' "language": "' + LanguageCode + '"' + #13#10 +
'}';
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
UpdateLanguageConfig();
end;
end;
+135
View File
@@ -0,0 +1,135 @@
; Script generated by the Inno Setup Script Wizard.
#define MyAppName "YTSage"
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#define MyAppPublisher "oop7"
#define MyAppURL "https://github.com/oop7/YTSage/"
#ifndef MyAppExeName
#define MyAppExeName "YTSage.exe"
#endif
#ifndef SourceDir
#define SourceDir "..\dist\YTSage"
#endif
[Setup]
AppId={{AE618DBF-DD56-462D-9C09-2C2B7A41B201}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
LicenseFile=..\LICENSE
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputBaseFilename=YTSage-v{#MyAppVersion}-Setup
SolidCompression=yes
WizardStyle=modern
OutputDir=..\artifacts
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "arabic"; MessagesFile: "compiler:Languages\Arabic.isl"
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
Name: "chinesesimplified"; MessagesFile: "Languages\Unofficial\ChineseSimplified.isl"
Name: "hindi"; MessagesFile: "Languages\Unofficial\Hindi.islu"
Name: "indonesian"; MessagesFile: "Languages\Unofficial\Indonesian.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Files]
Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Code]
procedure UpdateLanguageConfig;
var
LanguageCode: string;
ConfigPath: string;
FileContent: AnsiString;
JsonContent: string;
LangPattern: string;
P_Start, P_End: Integer;
begin
if ActiveLanguage = 'english' then LanguageCode := 'en'
else if ActiveLanguage = 'arabic' then LanguageCode := 'ar'
else if ActiveLanguage = 'brazilianportuguese' then LanguageCode := 'pt-br'
else if ActiveLanguage = 'french' then LanguageCode := 'fr'
else if ActiveLanguage = 'german' then LanguageCode := 'de'
else if ActiveLanguage = 'italian' then LanguageCode := 'it'
else if ActiveLanguage = 'japanese' then LanguageCode := 'ja'
else if ActiveLanguage = 'polish' then LanguageCode := 'pl'
else if ActiveLanguage = 'portuguese' then LanguageCode := 'pt'
else if ActiveLanguage = 'russian' then LanguageCode := 'ru'
else if ActiveLanguage = 'spanish' then LanguageCode := 'es'
else if ActiveLanguage = 'turkish' then LanguageCode := 'tr'
else if ActiveLanguage = 'chinesesimplified' then LanguageCode := 'zh'
else if ActiveLanguage = 'hindi' then LanguageCode := 'hi'
else if ActiveLanguage = 'indonesian' then LanguageCode := 'id'
else LanguageCode := 'en';
ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json');
if not DirExists(ExtractFilePath(ConfigPath)) then
ForceDirectories(ExtractFilePath(ConfigPath));
if FileExists(ConfigPath) then
begin
if LoadStringFromFile(ConfigPath, FileContent) then
begin
JsonContent := String(FileContent);
LangPattern := '"language": "';
P_Start := Pos(LangPattern, JsonContent);
if P_Start > 0 then
begin
P_Start := P_Start + Length(LangPattern);
P_End := Pos('"', Copy(JsonContent, P_Start, Length(JsonContent)));
if P_End > 0 then
begin
Delete(JsonContent, P_Start, P_End - 1);
Insert(LanguageCode, JsonContent, P_Start);
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
end;
end
else
begin
JsonContent := '{' + #13#10 +
' "language": "' + LanguageCode + '"' + #13#10 +
'}';
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
UpdateLanguageConfig();
end;
end;
@@ -1,581 +0,0 @@
"""
History Dialog for YTSage application.
Displays download history with thumbnails and provides options to redownload or remove entries.
"""
import os
import subprocess
from datetime import datetime
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Optional
import requests
from PIL import Image
from PySide6.QtCore import Qt, QSize, Signal
from PySide6.QtGui import QPixmap, QIcon
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QScrollArea,
QWidget,
QFrame,
QMenu,
QMessageBox,
QSizePolicy,
)
from src.utils.ytsage_history_manager import HistoryManager
from src.utils.ytsage_constants import APP_THUMBNAILS_DIR, SUBPROCESS_CREATIONFLAGS
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp
class HistoryEntryWidget(QFrame):
"""Widget representing a single history entry."""
remove_requested = Signal(str) # Emit entry ID when remove is requested
redownload_requested = Signal(dict) # Emit entry data when redownload is requested
def __init__(self, entry: dict, parent=None):
super().__init__(parent)
self.entry = entry
self.entry_id = entry.get("id", "")
self.setup_ui()
def setup_ui(self):
"""Setup the UI for this history entry."""
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised)
self.setStyleSheet("""
QFrame {
background-color: #1d1e22;
border: 1px solid #2a2d36;
border-radius: 8px;
padding: 10px;
margin: 5px;
}
QFrame:hover {
background-color: #252830;
border-color: #3a3d46;
}
""")
main_layout = QHBoxLayout(self)
main_layout.setSpacing(15)
main_layout.setContentsMargins(10, 10, 10, 10)
# Thumbnail - Larger size to utilize available space
self.thumbnail_label = QLabel()
self.thumbnail_label.setFixedSize(280, 158) # 16:9 ratio, larger to fill space
self.thumbnail_label.setStyleSheet("""
QLabel {
border: 2px solid #3d3d3d;
border-radius: 6px;
background-color: #15181b;
}
""")
self.thumbnail_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.thumbnail_label.setScaledContents(True)
# Load thumbnail
self.load_thumbnail()
main_layout.addWidget(self.thumbnail_label, alignment=Qt.AlignmentFlag.AlignTop)
# Info section
info_layout = QVBoxLayout()
info_layout.setSpacing(5)
# Title
title = self.entry.get("title", _("video_info.unknown_title"))
self.title_label = QLabel(title)
self.title_label.setWordWrap(True)
self.title_label.setStyleSheet("""
QLabel {
font-size: 14px;
font-weight: bold;
color: #ffffff;
}
""")
info_layout.addWidget(self.title_label)
# Channel (if available)
channel = self.entry.get("channel")
if channel:
channel_label = QLabel(f"{_('video_info.channel')}: {channel}")
channel_label.setStyleSheet("color: #cccccc; font-size: 12px;")
info_layout.addWidget(channel_label)
# Download date
download_date = self.entry.get("download_date", "")
if download_date:
try:
dt = datetime.fromisoformat(download_date)
date_str = dt.strftime("%Y-%m-%d %H:%M")
date_label = QLabel(_("history.downloaded_on", date=date_str))
date_label.setStyleSheet("color: #aaaaaa; font-size: 11px;")
info_layout.addWidget(date_label)
except Exception as e:
logger.debug(f"Error parsing date: {e}")
# File size and type
file_size = self.entry.get("file_size", 0)
is_audio = self.entry.get("is_audio_only", False)
size_type_layout = QHBoxLayout()
# File type badge
type_badge = QLabel(_("history.audio_download") if is_audio else _("history.video_download"))
type_badge.setStyleSheet(f"""
QLabel {{
background-color: {'#c90000' if not is_audio else '#0066cc'};
color: white;
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}}
""")
size_type_layout.addWidget(type_badge)
# File size
if file_size > 0:
size_str = self.format_file_size(file_size)
size_label = QLabel(_("history.file_size", size=size_str))
size_label.setStyleSheet("color: #aaaaaa; font-size: 11px;")
size_type_layout.addWidget(size_label)
size_type_layout.addStretch()
info_layout.addLayout(size_type_layout)
info_layout.addStretch()
main_layout.addLayout(info_layout, 1)
# Three-dot menu button
self.menu_button = QPushButton("")
self.menu_button.setFixedSize(40, 40)
self.menu_button.setStyleSheet("""
QPushButton {
background-color: #2a2d36;
border: none;
border-radius: 20px;
color: white;
font-size: 24px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3a3d46;
}
QPushButton:pressed {
background-color: #c90000;
}
""")
self.menu_button.clicked.connect(self.show_menu)
main_layout.addWidget(self.menu_button, alignment=Qt.AlignmentFlag.AlignTop)
def load_thumbnail(self):
"""Load and display the thumbnail."""
thumbnail_url = self.entry.get("thumbnail_url")
if not thumbnail_url:
self.set_placeholder_thumbnail()
return
# Check if thumbnail is cached
thumbnail_filename = f"{self.entry_id}.jpg"
thumbnail_path = APP_THUMBNAILS_DIR / thumbnail_filename
if thumbnail_path.exists():
try:
pixmap = QPixmap(str(thumbnail_path))
if not pixmap.isNull():
# Don't scale here, let setScaledContents handle it
self.thumbnail_label.setPixmap(pixmap)
return
except Exception as e:
logger.debug(f"Error loading cached thumbnail: {e}")
# Download thumbnail
try:
response = requests.get(thumbnail_url, timeout=5)
response.raise_for_status()
image = Image.open(BytesIO(response.content))
# Don't resize, keep original quality and just save at higher quality
# The QPixmap scaling will handle the display size with high quality
# Save to cache with higher quality
try:
APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
image.save(thumbnail_path, "JPEG", quality=95, optimize=True)
except Exception as e:
logger.debug(f"Error caching thumbnail: {e}")
# Convert to QPixmap
image_bytes = BytesIO()
image.save(image_bytes, format="JPEG", quality=95)
image_bytes.seek(0)
pixmap = QPixmap()
pixmap.loadFromData(image_bytes.read())
if not pixmap.isNull():
# Don't scale here, let setScaledContents handle it
self.thumbnail_label.setPixmap(pixmap)
else:
self.set_placeholder_thumbnail()
except Exception as e:
logger.debug(f"Error downloading thumbnail: {e}")
self.set_placeholder_thumbnail()
def set_placeholder_thumbnail(self):
"""Set a placeholder when thumbnail is not available."""
self.thumbnail_label.setText("📹" if not self.entry.get("is_audio_only") else "🎵")
self.thumbnail_label.setStyleSheet("""
QLabel {
border: 1px solid #3d3d3d;
border-radius: 4px;
background-color: #15181b;
color: #666666;
font-size: 48px;
}
""")
def format_file_size(self, size_bytes: int) -> str:
"""Format file size in human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def show_menu(self):
"""Show the context menu with options."""
menu = QMenu(self)
menu.setStyleSheet("""
QMenu {
background-color: #2a2d36;
border: 1px solid #3a3d46;
color: white;
padding: 5px;
}
QMenu::item {
padding: 8px 20px;
border-radius: 4px;
}
QMenu::item:selected {
background-color: #c90000;
}
""")
# Open file location
open_action = menu.addAction("📁 " + _("history.open_location"))
open_action.triggered.connect(self.open_file_location)
# Redownload
redownload_action = menu.addAction("⬇️ " + _("history.redownload"))
redownload_action.triggered.connect(self.redownload)
menu.addSeparator()
# Remove from history
remove_action = menu.addAction("🗑️ " + _("history.remove"))
remove_action.triggered.connect(self.remove_from_history)
# Show menu at button position
menu.exec(self.menu_button.mapToGlobal(self.menu_button.rect().bottomLeft()))
def open_file_location(self):
"""Open the file location in the system file explorer."""
file_path = Path(self.entry.get("file_path", ""))
if not file_path.exists():
QMessageBox.warning(
self,
_("history.file_not_found"),
_("history.file_not_found_message", path=str(file_path))
)
return
try:
# On Windows, use explorer with /select to highlight the file
if os.name == "nt":
subprocess.run(['explorer', '/select,', str(file_path)], creationflags=SUBPROCESS_CREATIONFLAGS)
# On macOS, use open with -R to reveal in Finder
elif subprocess.sys.platform == "darwin":
subprocess.run(['open', '-R', str(file_path)])
# On Linux, try to open the folder
else:
folder_path = file_path.parent
subprocess.run(['xdg-open', str(folder_path)])
logger.info(f"Opened file location: {file_path}")
except Exception as e:
logger.exception(f"Error opening file location: {e}")
QMessageBox.warning(self, "Error", f"Could not open file location: {str(e)}")
def redownload(self):
"""Request redownload of this entry."""
reply = QMessageBox.question(
self,
_("history.redownload_confirm_title"),
_("history.redownload_confirm_message", title=self.entry.get("title", "")),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.redownload_requested.emit(self.entry)
def remove_from_history(self):
"""Request removal of this entry from history."""
reply = QMessageBox.question(
self,
_("history.remove_confirm_title"),
_("history.remove_confirm_message", title=self.entry.get("title", "")),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.remove_requested.emit(self.entry_id)
class HistoryDialog(QDialog):
"""Dialog to display and manage download history."""
redownload_requested = Signal(dict) # Signal to request redownload in main window
def __init__(self, parent: Optional["YTSageApp"] = None):
super().__init__(parent)
self.parent_app = parent
self.entry_widgets = []
self.setup_ui()
self.load_history()
def setup_ui(self):
"""Setup the dialog UI."""
self.setWindowTitle(_("history.title"))
self.setMinimumSize(700, 500)
self.resize(850, 600)
# Set window flags
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint)
layout = QVBoxLayout(self)
layout.setSpacing(10)
layout.setContentsMargins(20, 20, 20, 20)
# Header with title and buttons
header_layout = QHBoxLayout()
title_label = QLabel(_("history.title"))
title_label.setStyleSheet("""
QLabel {
font-size: 18px;
font-weight: bold;
color: white;
}
""")
header_layout.addWidget(title_label)
header_layout.addStretch()
# Clear all button
self.clear_all_btn = QPushButton(_("history.clear_all"))
self.clear_all_btn.setStyleSheet("""
QPushButton {
background-color: #c90000;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
""")
self.clear_all_btn.clicked.connect(self.clear_all_history)
header_layout.addWidget(self.clear_all_btn)
layout.addLayout(header_layout)
# Search bar
self.search_input = QLineEdit()
self.search_input.setPlaceholderText(_("history.search_placeholder"))
self.search_input.setStyleSheet("""
QLineEdit {
padding: 10px;
border: 2px solid #1b2021;
border-radius: 4px;
background-color: #1b2021;
color: #ffffff;
font-size: 13px;
}
""")
self.search_input.textChanged.connect(self.filter_history)
layout.addWidget(self.search_input)
# Scroll area for history entries
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background-color: transparent;
}
""")
# Container for history entries
self.history_container = QWidget()
self.history_layout = QVBoxLayout(self.history_container)
self.history_layout.setSpacing(10)
self.history_layout.setContentsMargins(0, 0, 0, 0)
self.history_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
scroll_area.setWidget(self.history_container)
layout.addWidget(scroll_area)
# Status label
self.status_label = QLabel()
self.status_label.setStyleSheet("color: #aaaaaa; font-size: 12px;")
layout.addWidget(self.status_label)
# Apply dark theme
self.setStyleSheet("""
QDialog {
background-color: #15181b;
}
QLabel {
color: #ffffff;
}
""")
def load_history(self):
"""Load and display history entries."""
# Clear existing widgets
for widget in self.entry_widgets:
widget.deleteLater()
self.entry_widgets.clear()
# Get history entries
entries = HistoryManager.get_all_entries()
if not entries:
self.show_empty_state()
return
# Create widgets for each entry
for entry in entries:
widget = HistoryEntryWidget(entry, self.history_container)
widget.remove_requested.connect(self.remove_entry)
widget.redownload_requested.connect(self.handle_redownload)
self.history_layout.addWidget(widget)
self.entry_widgets.append(widget)
# Update status
count = len(entries)
if count == 1:
status_text = _("history.one_entry")
else:
status_text = _("history.entries_count", count=count)
self.status_label.setText(status_text)
def show_empty_state(self):
"""Show empty state when there's no history."""
empty_widget = QWidget()
empty_layout = QVBoxLayout(empty_widget)
empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
icon_label = QLabel("📂")
icon_label.setStyleSheet("font-size: 64px; color: #555555;")
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
empty_layout.addWidget(icon_label)
title_label = QLabel(_("history.no_history"))
title_label.setStyleSheet("font-size: 16px; color: #888888; font-weight: bold;")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
empty_layout.addWidget(title_label)
desc_label = QLabel(_("history.no_history_description"))
desc_label.setStyleSheet("font-size: 13px; color: #666666;")
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
empty_layout.addWidget(desc_label)
self.history_layout.addWidget(empty_widget)
self.entry_widgets.append(empty_widget)
self.status_label.setText("")
self.clear_all_btn.setEnabled(False)
def filter_history(self, query: str):
"""Filter history entries based on search query."""
if not query:
# Show all entries
for widget in self.entry_widgets:
widget.show()
return
# Hide/show based on query
query_lower = query.lower()
visible_count = 0
for widget in self.entry_widgets:
if isinstance(widget, HistoryEntryWidget):
title = (widget.entry.get("title") or "").lower()
channel = (widget.entry.get("channel") or "").lower()
if query_lower in title or query_lower in channel:
widget.show()
visible_count += 1
else:
widget.hide()
def remove_entry(self, entry_id: str):
"""Remove an entry from history."""
success = HistoryManager.remove_entry(entry_id)
if success:
# Reload history
self.load_history()
logger.info(f"Removed entry from history: {entry_id}")
else:
QMessageBox.warning(self, "Error", "Failed to remove entry from history")
def clear_all_history(self):
"""Clear all history entries."""
reply = QMessageBox.question(
self,
_("history.clear_confirm_title"),
_("history.clear_confirm_message"),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
count = HistoryManager.clear_history()
self.load_history()
logger.info(f"Cleared all history: {count} entries")
def handle_redownload(self, entry: dict):
"""Handle redownload request."""
# Emit signal to parent window
self.redownload_requested.emit(entry)
# Close dialog
self.accept()
View File
-348
View File
@@ -1,348 +0,0 @@
"""
History Manager Module
======================
This module provides **thread-safe** centralized management for download
history in YTSage. It handles reading, writing, and managing download history
stored in a JSON file.
Thread safety is ensured using a reentrant lock (`RLock`), so multiple threads
can safely access or modify history concurrently.
Features
--------
- Thread-safe operations for getting, adding, and removing history entries.
- Loads history from a JSON file (`APP_HISTORY_FILE`).
- Creates the history file if missing or corrupt.
- Manages download history with metadata including thumbnails, file paths, and download options.
- Provides safe error handling with logging instead of raising exceptions.
- Persists updates back to disk automatically.
Usage
-----
from src.utils.ytsage_history_manager import HistoryManager
# Add a download to history
HistoryManager.add_entry(
title="Video Title",
url="https://youtube.com/watch?v=...",
thumbnail_url="https://...",
file_path="/path/to/file.mp4",
format_id="137+140",
is_audio_only=False,
resolution="1080p",
download_options={...}
)
# Get all history entries
history = HistoryManager.get_all_entries()
# Remove an entry
HistoryManager.remove_entry(entry_id)
# Clear all history
HistoryManager.clear_history()
Design Notes
------------
- History entries are stored in `HistoryManager._history` (a list of dicts).
- Each entry has a unique ID based on timestamp.
- All modifications trigger a save (`_save`) to keep JSON in sync.
- Logs actions and errors using the app's central logger.
- Uses `RLock` to allow safe concurrent access from multiple threads.
Exceptions
----------
- Any issues during file I/O (permissions, disk errors, JSON corruption)
are caught and logged. The application continues running with an empty
history when possible.
"""
import json
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from src.utils.ytsage_constants import APP_HISTORY_FILE
from src.utils.ytsage_logger import logger
class HistoryManager:
"""
Thread-safe history manager for YTSage.
Provides methods to load, save, get, add, and remove download history entries.
Automatically persists changes to disk.
"""
_lock = threading.RLock()
_history_file = APP_HISTORY_FILE
_history: List[Dict[str, Any]] = []
_loaded = False
@classmethod
def _load(cls) -> None:
"""
Loads download history from a JSON file if it exists and is valid.
If the file is missing or corrupt, initializes with an empty history.
Logs actions and errors during the process.
"""
with cls._lock:
if cls._history_file.exists():
try:
with open(cls._history_file, "r", encoding="utf-8") as f:
data = json.load(f)
# Ensure it's a list
if isinstance(data, list):
cls._history = data
else:
cls._history = []
logger.warning("History file format invalid, initialized empty history.")
logger.info(f"History loaded from file: {len(cls._history)} entries.")
except json.JSONDecodeError:
cls._history = []
logger.warning("History file corrupt, initialized empty history.")
except Exception as e:
cls._history = []
logger.error(f"Error loading history file: {e}")
else:
cls._history = []
cls._save()
logger.info("History file not found, created empty history.")
cls._loaded = True
@classmethod
def _save(cls) -> None:
"""
Save current history to JSON file.
Note:
May raise exceptions if the file cannot be written due to permission issues,
disk errors, or other I/O problems.
"""
with cls._lock:
try:
# Ensure parent directory exists
cls._history_file.parent.mkdir(parents=True, exist_ok=True)
with open(cls._history_file, "w", encoding="utf-8") as f:
json.dump(cls._history, f, indent=2, ensure_ascii=False)
logger.debug(f"History saved to file: {len(cls._history)} entries.")
except (OSError, PermissionError) as e:
logger.exception(f"Failed to save history: {e}")
except Exception as e:
logger.exception(f"Unexpected error while saving history: {e}")
@classmethod
def _ensure_loaded(cls) -> None:
"""Ensure history is loaded before any operation."""
with cls._lock:
if not cls._loaded:
cls._load()
@classmethod
def add_entry(
cls,
title: str,
url: str,
thumbnail_url: Optional[str],
file_path: str,
format_id: str,
is_audio_only: bool,
resolution: str,
file_size: Optional[int] = None,
channel: Optional[str] = None,
duration: Optional[str] = None,
download_options: Optional[Dict[str, Any]] = None,
) -> str:
"""
Add a new download entry to history.
Args:
title: Video/audio title
url: Original URL
thumbnail_url: Thumbnail URL (can be None)
file_path: Path to downloaded file
format_id: Format ID used for download
is_audio_only: Whether it's audio-only download
resolution: Resolution string (e.g., "1080p", "best audio")
file_size: File size in bytes (optional)
channel: Channel name (optional)
duration: Duration string (optional)
download_options: Dictionary of all download options used (optional)
Returns:
str: The unique ID of the created entry
"""
cls._ensure_loaded()
with cls._lock:
# Generate unique ID based on timestamp
entry_id = f"{int(time.time() * 1000)}"
# Get file size if not provided
if file_size is None:
try:
file_path_obj = Path(file_path)
if file_path_obj.exists():
file_size = file_path_obj.stat().st_size
except Exception as e:
logger.debug(f"Could not get file size: {e}")
file_size = 0
entry = {
"id": entry_id,
"title": title,
"url": url,
"thumbnail_url": thumbnail_url,
"file_path": file_path,
"download_date": datetime.now().isoformat(),
"format_id": format_id,
"is_audio_only": is_audio_only,
"resolution": resolution,
"file_size": file_size or 0,
"channel": channel,
"duration": duration,
"download_options": download_options or {},
}
# Add to beginning of list (most recent first)
cls._history.insert(0, entry)
cls._save()
logger.info(f"Added entry to history: {title}")
return entry_id
@classmethod
def get_all_entries(cls, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Retrieve all history entries.
Args:
limit: Optional limit on number of entries to return (most recent first)
Returns:
List of history entry dictionaries
"""
cls._ensure_loaded()
with cls._lock:
if limit is not None:
return cls._history[:limit]
return cls._history.copy()
@classmethod
def get_entry(cls, entry_id: str) -> Optional[Dict[str, Any]]:
"""
Get a specific history entry by ID.
Args:
entry_id: The unique entry ID
Returns:
Entry dictionary or None if not found
"""
cls._ensure_loaded()
with cls._lock:
for entry in cls._history:
if entry.get("id") == entry_id:
return entry.copy()
return None
@classmethod
def remove_entry(cls, entry_id: str) -> bool:
"""
Remove a specific entry from history.
Args:
entry_id: The unique entry ID to remove
Returns:
bool: True if entry was found and removed, False otherwise
"""
cls._ensure_loaded()
with cls._lock:
for i, entry in enumerate(cls._history):
if entry.get("id") == entry_id:
removed = cls._history.pop(i)
cls._save()
logger.info(f"Removed entry from history: {removed.get('title', 'Unknown')}")
return True
logger.debug(f"Entry ID '{entry_id}' not found in history.")
return False
@classmethod
def clear_history(cls) -> int:
"""
Clear all history entries.
Returns:
int: Number of entries that were cleared
"""
cls._ensure_loaded()
with cls._lock:
count = len(cls._history)
cls._history = []
cls._save()
logger.info(f"Cleared all history: {count} entries removed.")
return count
@classmethod
def search_entries(cls, query: str) -> List[Dict[str, Any]]:
"""
Search history entries by title, channel, or URL.
Args:
query: Search query string
Returns:
List of matching history entries
"""
cls._ensure_loaded()
if not query:
return cls.get_all_entries()
query_lower = query.lower()
with cls._lock:
results = []
for entry in cls._history:
# Search in title, channel, and URL
title = (entry.get("title") or "").lower()
channel = (entry.get("channel") or "").lower()
url = (entry.get("url") or "").lower()
if query_lower in title or query_lower in channel or query_lower in url:
results.append(entry.copy())
return results
@classmethod
def get_statistics(cls) -> Dict[str, Any]:
"""
Get statistics about download history.
Returns:
Dictionary with statistics (total_downloads, total_size, etc.)
"""
cls._ensure_loaded()
with cls._lock:
total_downloads = len(cls._history)
total_size = sum(entry.get("file_size", 0) for entry in cls._history)
video_count = sum(1 for entry in cls._history if not entry.get("is_audio_only", False))
audio_count = sum(1 for entry in cls._history if entry.get("is_audio_only", False))
return {
"total_downloads": total_downloads,
"total_size": total_size,
"video_count": video_count,
"audio_count": audio_count,
}
+1 -1
View File
@@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader
A modern, user-friendly YouTube video downloader built with PySide6. A modern, user-friendly YouTube video downloader built with PySide6.
""" """
__version__ = "4.9.7" __version__ = "5.0.0b5"
__author__ = "oop7" __author__ = "oop7"

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -19,9 +19,9 @@ from PySide6.QtWidgets import (
QVBoxLayout, QVBoxLayout,
) )
from src.utils.ytsage_logger import logger from ..utils.ytsage_logger import logger
from src.utils.ytsage_localization import _ from ..utils.ytsage_localization import _
from src.utils.ytsage_constants import ( from ..utils.ytsage_constants import (
APP_BIN_DIR, APP_BIN_DIR,
ICON_PATH, ICON_PATH,
OS_FULL_NAME, OS_FULL_NAME,
@@ -31,7 +31,7 @@ from src.utils.ytsage_constants import (
DENO_DOWNLOAD_URL, DENO_DOWNLOAD_URL,
DENO_SHA256_URL, DENO_SHA256_URL,
) )
from src.core.ytsage_ffmpeg import get_file_sha256 from .ytsage_ffmpeg import get_file_sha256
def verify_deno_sha256(file_path: Path, sha256_url: str) -> bool: def verify_deno_sha256(file_path: Path, sha256_url: str) -> bool:
@@ -171,8 +171,9 @@ class DownloadDenoThread(QThread):
self.finished_signal.emit(False, f"Executable '{executable_name}' not found in zip") self.finished_signal.emit(False, f"Executable '{executable_name}' not found in zip")
return return
# Extract to app bin directory # Extract to app bin directory (while zip_ref is open)
zip_ref.extract(executable_name, APP_BIN_DIR) target_dir = DENO_APP_BIN_PATH.parent
zip_ref.extract(executable_name, target_dir)
# Verify the extracted file exists # Verify the extracted file exists
exe_path = DENO_APP_BIN_PATH exe_path = DENO_APP_BIN_PATH
@@ -682,10 +683,13 @@ def compare_deno_versions(current: str, latest: str) -> bool:
return False return False
def upgrade_deno() -> tuple[bool, str]: def upgrade_deno(progress_callback=None) -> tuple[bool, str]:
""" """
Upgrade Deno to the latest version using 'deno upgrade' command. Upgrade Deno to the latest version using 'deno upgrade' command.
Args:
progress_callback: Optional function to call with output lines for progress tracking
Returns: Returns:
tuple: (success: bool, output: str) - Success status and command output tuple: (success: bool, output: str) - Success status and command output
""" """
@@ -699,23 +703,42 @@ def upgrade_deno() -> tuple[bool, str]:
logger.info(f"Upgrading Deno using: {deno_path}") logger.info(f"Upgrading Deno using: {deno_path}")
# Run deno upgrade command # Run deno upgrade command with output capturing
result = subprocess.run( process = subprocess.Popen(
[str(deno_path), "upgrade"], [str(deno_path), "upgrade"],
capture_output=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True, text=True,
timeout=300, # 5 minutes timeout creationflags=SUBPROCESS_CREATIONFLAGS,
creationflags=SUBPROCESS_CREATIONFLAGS bufsize=1, # Line buffered
encoding='utf-8',
errors='replace'
) )
output = result.stdout + result.stderr full_output = []
if result.returncode == 0: # Read output line by line as it is generated
while True:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line_str = line.strip()
if line_str:
full_output.append(line_str)
logger.debug(f"Deno upgrade output: {line_str}")
if progress_callback:
progress_callback(line_str)
return_code = process.poll()
output = "\n".join(full_output)
if return_code == 0:
logger.info("Deno upgrade successful") logger.info("Deno upgrade successful")
logger.debug(f"Upgrade output: {output}")
return True, output return True, output
else: else:
logger.error(f"Deno upgrade failed with code {result.returncode}") logger.error(f"Deno upgrade failed with code {return_code}")
logger.error(f"Output: {output}") logger.error(f"Output: {output}")
return False, output return False, output
@@ -11,10 +11,16 @@ from typing import Optional, List, Set
from PySide6.QtCore import QObject, QThread, Signal from PySide6.QtCore import QObject, QThread, Signal
from src.core.ytsage_yt_dlp import get_yt_dlp_path from .ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS from ..utils.ytsage_constants import (
from src.utils.ytsage_localization import LocalizationManager SUBPROCESS_CREATIONFLAGS,
from src.utils.ytsage_logger import logger VIDEO_EXTENSIONS,
AUDIO_EXTENSIONS,
SUBTITLE_EXTENSIONS,
MEDIA_EXTENSIONS,
)
from ..utils.ytsage_localization import LocalizationManager
from ..utils.ytsage_logger import logger
# Shorthand for localization # Shorthand for localization
_ = LocalizationManager.get_text _ = LocalizationManager.get_text
@@ -66,6 +72,7 @@ class DownloadThread(QThread):
preferred_output_format="mp4", preferred_output_format="mp4",
force_audio_format=False, force_audio_format=False,
preferred_audio_format="best", preferred_audio_format="best",
filename_format=None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.url = url self.url = url
@@ -93,6 +100,7 @@ class DownloadThread(QThread):
self.preferred_output_format = preferred_output_format self.preferred_output_format = preferred_output_format
self.force_audio_format = force_audio_format self.force_audio_format = force_audio_format
self.preferred_audio_format = preferred_audio_format self.preferred_audio_format = preferred_audio_format
self.filename_format = filename_format
self.paused: bool = False self.paused: bool = False
self.cancelled: bool = False self.cancelled: bool = False
self.process: Optional[subprocess.Popen] = None self.process: Optional[subprocess.Popen] = None
@@ -189,9 +197,12 @@ class DownloadThread(QThread):
def safe_delete(path: Path) -> bool: def safe_delete(path: Path) -> bool:
try: try:
# Check if file exists before trying to delete
if path.exists():
path.unlink(missing_ok=True) path.unlink(missing_ok=True)
logger.debug(f"Deleted subtitle file: {path.name}") logger.debug(f"Deleted subtitle file: {path.name}")
return True return True
return False
except Exception as e: except Exception as e:
logger.exception(f"Error deleting subtitle file {path}: {e}") logger.exception(f"Error deleting subtitle file {path}: {e}")
return False return False
@@ -265,11 +276,14 @@ class DownloadThread(QThread):
# Use string concatenation instead of Path.joinpath to avoid Path object issues # Use string concatenation instead of Path.joinpath to avoid Path object issues
base_path: str = self.path.as_posix() base_path: str = self.path.as_posix()
# Determine the filename part of the template
filename_part = self.filename_format if self.filename_format else "%(title)s_%(resolution)s.%(ext)s"
if self.is_playlist: if self.is_playlist:
# Create output template with playlist subfolder # Create output template with playlist subfolder
output_template: str = f"{base_path}/%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s" output_template: str = f"{base_path}/%(playlist_title)s/{filename_part}"
else: else:
output_template: str = f"{base_path}/%(title)s_%(resolution)s.%(ext)s" output_template: str = f"{base_path}/{filename_part}"
cmd.extend(["-o", str(output_template)]) cmd.extend(["-o", str(output_template)])
@@ -376,7 +390,9 @@ class DownloadThread(QThread):
def _run_direct_command(self) -> None: def _run_direct_command(self) -> None:
"""Run yt-dlp as a direct command line process instead of using Python API.""" """Run yt-dlp as a direct command line process instead of using Python API."""
try: try:
self.error_lines = [] # Initialize error capture list
cmd: List[str] = self._build_yt_dlp_command() cmd: List[str] = self._build_yt_dlp_command()
cmd_str: str = " ".join(shlex.quote(str(arg)) for arg in cmd) cmd_str: str = " ".join(shlex.quote(str(arg)) for arg in cmd)
logger.debug(f"Executing command: {cmd_str}") logger.debug(f"Executing command: {cmd_str}")
@@ -410,8 +426,6 @@ class DownloadThread(QThread):
self._terminate_process_tree(self.process) self._terminate_process_tree(self.process)
# Add delay before cleanup to allow file handles to be released # Add delay before cleanup to allow file handles to be released
# Force garbage collection to help release resources
gc.collect()
time.sleep(2) time.sleep(2)
self.cleanup_partial_files() self.cleanup_partial_files()
self.status_signal.emit(_("download.cancelled")) self.status_signal.emit(_("download.cancelled"))
@@ -443,10 +457,6 @@ class DownloadThread(QThread):
final_file_found = False final_file_found = False
try: try:
# Define video/audio extensions
video_audio_extensions = {'.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv',
'.m4a', '.mp3', '.opus', '.flac', '.aac', '.wav', '.ogg'}
# First, check if last_file_path exists and is valid # First, check if last_file_path exists and is valid
if self.last_file_path: if self.last_file_path:
last_path = Path(self.last_file_path) last_path = Path(self.last_file_path)
@@ -462,7 +472,7 @@ class DownloadThread(QThread):
potential_files = [] potential_files = []
# Search in download directory and subdirectories (for playlists) # Search in download directory and subdirectories (for playlists)
for ext in video_audio_extensions: for ext in MEDIA_EXTENSIONS:
potential_files.extend(self.path.glob(f'*{ext}')) potential_files.extend(self.path.glob(f'*{ext}'))
# Also check subdirectories (for playlist downloads) # Also check subdirectories (for playlist downloads)
potential_files.extend(self.path.glob(f'*/*{ext}')) potential_files.extend(self.path.glob(f'*/*{ext}'))
@@ -505,13 +515,18 @@ class DownloadThread(QThread):
if self.cancelled: if self.cancelled:
self.status_signal.emit(_("download.cancelled")) self.status_signal.emit(_("download.cancelled"))
else: else:
# Provide more descriptive error message for possible yt-dlp conflicts # Provide informative error message based on captured output
if return_code == 1: if self.error_lines:
# Use the captured error lines (last 2 for context)
error_msg = "\n".join(self.error_lines[-2:])
self.error_signal.emit( self.error_signal.emit(
f"Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application." _("errors.ytdlp_failed", error=error_msg)
) )
else: else:
self.error_signal.emit(f"Download failed with return code {return_code}") # Fallback to generic return code error
self.error_signal.emit(
_("errors.download_failed_return_code", return_code=return_code)
)
# Add delay before cleanup to allow file handles to be released # Add delay before cleanup to allow file handles to be released
time.sleep(1) time.sleep(1)
@@ -519,7 +534,7 @@ class DownloadThread(QThread):
except Exception as e: except Exception as e:
logger.exception(f"Error in direct command: {e}") logger.exception(f"Error in direct command: {e}")
self.error_signal.emit(f"Error in direct command: {e}") self.error_signal.emit(_("errors.direct_command_error", error=str(e)))
# Add delay before cleanup to allow file handles to be released # Add delay before cleanup to allow file handles to be released
time.sleep(1) time.sleep(1)
self.cleanup_partial_files() self.cleanup_partial_files()
@@ -529,6 +544,11 @@ class DownloadThread(QThread):
line = line.strip() line = line.strip()
# logger.info(f"yt-dlp: {line}") # Log all output - OPTIONALLY UNCOMMENT FOR VERBOSE DEBUG # logger.info(f"yt-dlp: {line}") # Log all output - OPTIONALLY UNCOMMENT FOR VERBOSE DEBUG
# Capture error lines
if "ERROR:" in line:
if hasattr(self, 'error_lines'):
self.error_lines.append(line)
# Extract filename when the destination line appears # Extract filename when the destination line appears
# Use a slightly more robust regex looking for the start of the line # Use a slightly more robust regex looking for the start of the line
dest_match = re.search(r"^\[download\] Destination:\s*(.*)", line) dest_match = re.search(r"^\[download\] Destination:\s*(.*)", line)
@@ -562,13 +582,13 @@ class DownloadThread(QThread):
if is_audio_download or "Downloading audio" in line: if is_audio_download or "Downloading audio" in line:
self.status_signal.emit(_("download.downloading_audio")) self.status_signal.emit(_("download.downloading_audio"))
# Video file extensions with likely video content # Video file extensions with likely video content
elif ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: elif ext in VIDEO_EXTENSIONS:
self.status_signal.emit(_("download.downloading_video")) self.status_signal.emit(_("download.downloading_video"))
# Audio file extensions # Audio file extensions
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: elif ext in AUDIO_EXTENSIONS:
self.status_signal.emit(_("download.downloading_audio")) self.status_signal.emit(_("download.downloading_audio"))
# Subtitle file extensions # Subtitle file extensions
elif ext in [".vtt", ".srt", ".ass", ".ssa"]: elif ext in SUBTITLE_EXTENSIONS:
self.status_signal.emit(_("download.downloading_subtitle")) self.status_signal.emit(_("download.downloading_subtitle"))
# Default case # Default case
else: else:
@@ -622,6 +642,14 @@ class DownloadThread(QThread):
if "Downloading webpage" in line or "Extracting URL" in line: if "Downloading webpage" in line or "Extracting URL" in line:
self.status_signal.emit(_("download.fetching_info")) self.status_signal.emit(_("download.fetching_info"))
self.progress_signal.emit(0) self.progress_signal.emit(0)
elif "[download] Destination:" in line:
# Extract the destination filename
match = re.search(r"Destination: (.+)", line)
if match:
dest_path = match.group(1).strip()
self.current_filename = Path(dest_path).name
self.last_file_path = dest_path
logger.debug(f"Captured destination filename: {self.current_filename}")
elif "Downloading API JSON" in line: elif "Downloading API JSON" in line:
self.status_signal.emit(_("download.processing_playlist")) self.status_signal.emit(_("download.processing_playlist"))
self.progress_signal.emit(0) self.progress_signal.emit(0)
@@ -695,11 +723,11 @@ class DownloadThread(QThread):
# Determine file type based on extension for existing file message # Determine file type based on extension for existing file message
ext = Path(filename).suffix.lower() ext = Path(filename).suffix.lower()
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: if ext in VIDEO_EXTENSIONS:
self.status_signal.emit(f"⚠️ Video file already exists") self.status_signal.emit(f"⚠️ Video file already exists")
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: elif ext in AUDIO_EXTENSIONS:
self.status_signal.emit(f"⚠️ Audio file already exists") self.status_signal.emit(f"⚠️ Audio file already exists")
elif ext in [".vtt", ".srt", ".ass", ".ssa"]: elif ext in SUBTITLE_EXTENSIONS:
self.status_signal.emit(f"⚠️ Subtitle file already exists") self.status_signal.emit(f"⚠️ Subtitle file already exists")
else: else:
self.status_signal.emit(f"⚠️ File already exists") self.status_signal.emit(f"⚠️ File already exists")
@@ -716,13 +744,13 @@ class DownloadThread(QThread):
ext = Path(self.current_filename).suffix.lower() ext = Path(self.current_filename).suffix.lower()
# Video file extensions # Video file extensions
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: if ext in VIDEO_EXTENSIONS:
self.status_signal.emit(_("download.video_completed")) self.status_signal.emit(_("download.video_completed"))
# Audio file extensions # Audio file extensions
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: elif ext in AUDIO_EXTENSIONS:
self.status_signal.emit(_("download.audio_completed")) self.status_signal.emit(_("download.audio_completed"))
# Subtitle file extensions # Subtitle file extensions
elif ext in [".vtt", ".srt", ".ass", ".ssa"]: elif ext in SUBTITLE_EXTENSIONS:
self.status_signal.emit(_("download.subtitle_completed")) self.status_signal.emit(_("download.subtitle_completed"))
# Default case # Default case
else: else:
@@ -7,8 +7,8 @@ from pathlib import Path
import requests import requests
from src.utils.ytsage_logger import logger from ..utils.ytsage_logger import logger
from src.utils.ytsage_constants import ( from ..utils.ytsage_constants import (
FFMPEG_7Z_DOWNLOAD_URL, FFMPEG_7Z_DOWNLOAD_URL,
FFMPEG_7Z_SHA256_URL, FFMPEG_7Z_SHA256_URL,
FFMPEG_ZIP_DOWNLOAD_URL, FFMPEG_ZIP_DOWNLOAD_URL,
@@ -11,9 +11,9 @@ from typing import Any, Dict, Optional, Union
import requests import requests
from packaging import version from packaging import version
from src.core.ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path from .ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
from src.core.ytsage_yt_dlp import get_yt_dlp_path from .ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import ( from ..utils.ytsage_constants import (
APP_CONFIG_FILE, APP_CONFIG_FILE,
OS_NAME, OS_NAME,
SUBPROCESS_CREATIONFLAGS, SUBPROCESS_CREATIONFLAGS,
@@ -21,8 +21,8 @@ from src.utils.ytsage_constants import (
YTDLP_APP_BIN_PATH, YTDLP_APP_BIN_PATH,
YTDLP_DOWNLOAD_URL, YTDLP_DOWNLOAD_URL,
) )
from src.utils.ytsage_localization import _ from ..utils.ytsage_localization import _
from src.utils.ytsage_logger import logger from ..utils.ytsage_logger import logger
try: try:
from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError
@@ -107,9 +107,10 @@ def update_version_cache(tool_name: str, version_info: str, path: Optional[str],
def load_version_cache_from_config() -> None: def load_version_cache_from_config() -> None:
"""Load cached version info from config file.""" """Load cached version info from config file."""
from ..utils.ytsage_config_manager import ConfigManager
try: try:
config = load_config() cached_versions = ConfigManager.get("cached_versions") or {}
cached_versions = config.get("cached_versions", {})
for tool_name, cache_data in cached_versions.items(): for tool_name, cache_data in cached_versions.items():
if tool_name in _version_cache: if tool_name in _version_cache:
@@ -120,10 +121,10 @@ def load_version_cache_from_config() -> None:
def save_version_cache_to_config() -> None: def save_version_cache_to_config() -> None:
"""Save version cache to config file.""" """Save version cache to config file."""
from ..utils.ytsage_config_manager import ConfigManager
try: try:
config = load_config() ConfigManager.set("cached_versions", _version_cache.copy())
config["cached_versions"] = _version_cache.copy()
save_config(config)
except Exception as e: except Exception as e:
logger.exception(f"Error saving version cache: {e}") logger.exception(f"Error saving version cache: {e}")
@@ -178,7 +179,7 @@ def get_ffmpeg_version_cached() -> str:
def get_deno_version_cached() -> str: def get_deno_version_cached() -> str:
"""Get Deno version with caching support.""" """Get Deno version with caching support."""
try: try:
from src.core.ytsage_deno import get_deno_path from .ytsage_deno import get_deno_path
current_path = get_deno_path() current_path = get_deno_path()
@@ -189,7 +190,7 @@ def get_deno_version_cached() -> str:
return cached_version return cached_version
# Get fresh version info # Get fresh version info
from src.core.ytsage_deno import get_deno_version_direct from .ytsage_deno import get_deno_version_direct
version_info = get_deno_version_direct(current_path) version_info = get_deno_version_direct(current_path)
# Update cache # Update cache
@@ -214,7 +215,7 @@ def refresh_version_cache(force=False) -> bool:
update_version_cache("ffmpeg", version_info, "ffmpeg", force_save=True) update_version_cache("ffmpeg", version_info, "ffmpeg", force_save=True)
# Refresh Deno # Refresh Deno
from src.core.ytsage_deno import get_deno_path, get_deno_version_direct from .ytsage_deno import get_deno_path, get_deno_version_direct
deno_path = get_deno_path() deno_path = get_deno_path()
version_info = get_deno_version_direct(deno_path) version_info = get_deno_version_direct(deno_path)
update_version_cache("deno", version_info, deno_path, force_save=True) update_version_cache("deno", version_info, deno_path, force_save=True)
@@ -323,51 +324,7 @@ def get_ffmpeg_version_direct() -> str:
# get_app_data_dir() moved to src\utils\ytsage_constants.py # get_app_data_dir() moved to src\utils\ytsage_constants.py
# get_config_file_path() moved to src\utils\ytsage_constants.py # get_config_file_path() moved to src\utils\ytsage_constants.py
# ensure_app_data_dir() moved to src\utils\ytsage_constants.py # ensure_app_data_dir() moved to src\utils\ytsage_constants.py
# load_config() and save_config() removed - use ConfigManager instead
def load_config() -> Dict[str, Any]:
"""Load the application configuration from file."""
default_config: Dict[str, Any] = {
"download_path": str(USER_HOME_DIR / "Downloads"),
"speed_limit_value": None,
"speed_limit_unit_index": 0,
"cookie_file_path": None,
"last_used_cookie_file": None,
"auto_update_ytdlp": True, # Enable auto-update by default
"auto_update_frequency": "daily", # daily, weekly, or startup
"last_update_check": 0, # timestamp of last check
"cached_versions": {
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
},
}
try:
if APP_CONFIG_FILE.exists():
with open(APP_CONFIG_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
# Merge with defaults to ensure all keys exist
for key, value in default_config.items():
if key not in config:
config[key] = value
return config
except (json.JSONDecodeError, UnicodeError, Exception) as e:
logger.exception(f"Error reading config file: {e}")
# If config file is corrupted, create a new one with defaults
save_config(default_config)
return default_config
def save_config(config: Dict[str, Any]) -> bool:
"""Save the application configuration to file."""
try:
with open(APP_CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
logger.exception(f"Error saving config: {e}")
return False
def check_ffmpeg() -> bool: def check_ffmpeg() -> bool:
@@ -593,15 +550,15 @@ def update_yt_dlp() -> bool:
def should_check_for_auto_update() -> bool: def should_check_for_auto_update() -> bool:
"""Check if auto-update should be performed based on user settings.""" """Check if auto-update should be performed based on user settings."""
try: from ..utils.ytsage_config_manager import ConfigManager
config = load_config()
try:
# Check if auto-update is enabled # Check if auto-update is enabled
if not config.get("auto_update_ytdlp", False): if not ConfigManager.get("auto_update_ytdlp"):
return False return False
frequency: str = config.get("auto_update_frequency", "daily") frequency: str = ConfigManager.get("auto_update_frequency") or "daily"
last_check: float = config.get("last_update_check", 0) last_check: float = ConfigManager.get("last_update_check") or 0
current_time: float = time.time() current_time: float = time.time()
# Calculate time since last check # Calculate time since last check
@@ -623,6 +580,8 @@ def should_check_for_auto_update() -> bool:
def check_and_update_ytdlp_auto() -> bool: def check_and_update_ytdlp_auto() -> bool:
"""Perform automatic yt-dlp update check and update if needed.""" """Perform automatic yt-dlp update check and update if needed."""
from ..utils.ytsage_config_manager import ConfigManager
try: try:
logger.info("Performing automatic yt-dlp update check...") logger.info("Performing automatic yt-dlp update check...")
@@ -653,9 +612,7 @@ def check_and_update_ytdlp_auto() -> bool:
if update_yt_dlp(): if update_yt_dlp():
logger.info("Auto-update completed successfully!") logger.info("Auto-update completed successfully!")
# Update the last check timestamp # Update the last check timestamp
config = load_config() ConfigManager.set("last_update_check", time.time())
config["last_update_check"] = time.time()
save_config(config)
return True return True
else: else:
logger.info("Auto-update failed") logger.info("Auto-update failed")
@@ -663,9 +620,7 @@ def check_and_update_ytdlp_auto() -> bool:
else: else:
logger.info("yt-dlp is already up to date") logger.info("yt-dlp is already up to date")
# Still update the timestamp even if no update was needed # Still update the timestamp even if no update was needed
config = load_config() ConfigManager.set("last_update_check", time.time())
config["last_update_check"] = time.time()
save_config(config)
return True return True
except requests.RequestException as e: except requests.RequestException as e:
@@ -682,7 +637,7 @@ def check_and_update_ytdlp_auto() -> bool:
def get_auto_update_settings() -> Dict[str, Any]: def get_auto_update_settings() -> Dict[str, Any]:
"""Get current auto-update settings from config.""" """Get current auto-update settings from config."""
from src.utils.ytsage_config_manager import ConfigManager from ..utils.ytsage_config_manager import ConfigManager
enabled: Optional[bool] = ConfigManager.get("auto_update_ytdlp") enabled: Optional[bool] = ConfigManager.get("auto_update_ytdlp")
frequency: Optional[str] = ConfigManager.get("auto_update_frequency") frequency: Optional[str] = ConfigManager.get("auto_update_frequency")
@@ -698,7 +653,7 @@ def get_auto_update_settings() -> Dict[str, Any]:
def update_auto_update_settings(enabled: bool, frequency: str) -> bool: def update_auto_update_settings(enabled: bool, frequency: str) -> bool:
"""Update auto-update settings in config.""" """Update auto-update settings in config."""
try: try:
from src.utils.ytsage_config_manager import ConfigManager from ..utils.ytsage_config_manager import ConfigManager
ConfigManager.set("auto_update_ytdlp", enabled) ConfigManager.set("auto_update_ytdlp", enabled)
ConfigManager.set("auto_update_frequency", frequency) ConfigManager.set("auto_update_frequency", frequency)
@@ -20,8 +20,8 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.utils.ytsage_logger import logger from ..utils.ytsage_logger import logger
from src.utils.ytsage_constants import ( from ..utils.ytsage_constants import (
APP_BIN_DIR, APP_BIN_DIR,
ICON_PATH, ICON_PATH,
OS_FULL_NAME, OS_FULL_NAME,
@@ -31,7 +31,8 @@ from src.utils.ytsage_constants import (
YTDLP_DOWNLOAD_URL, YTDLP_DOWNLOAD_URL,
YTDLP_SHA256_URL, YTDLP_SHA256_URL,
) )
from src.core.ytsage_ffmpeg import get_file_sha256 from .ytsage_ffmpeg import get_file_sha256
from ..utils.ytsage_localization import _
# YTDLP_URLS moved to src\utils\ytsage_constants.py # YTDLP_URLS moved to src\utils\ytsage_constants.py
# get_ytdlp_install_dir() moved to src\utils\ytsage_constants.py # get_ytdlp_install_dir() moved to src\utils\ytsage_constants.py
@@ -162,7 +163,7 @@ class YtdlpSetupDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("yt-dlp Setup Required") self.setWindowTitle(_("ytdlp_setup.required_title"))
self.setMinimumWidth(520) self.setMinimumWidth(520)
self.setMinimumHeight(350) self.setMinimumHeight(350)
self.resize(520, 380) self.resize(520, 380)
@@ -253,7 +254,7 @@ class YtdlpSetupDialog(QDialog):
layout.setContentsMargins(25, 25, 25, 25) layout.setContentsMargins(25, 25, 25, 25)
# Header title # Header title
title_label = QLabel("yt-dlp Setup Required") title_label = QLabel(_("ytdlp_setup.required_title"))
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label) layout.addWidget(title_label)
@@ -262,10 +263,7 @@ class YtdlpSetupDialog(QDialog):
# os_name logic moved to src\utils\ytsage_constants.py # os_name logic moved to src\utils\ytsage_constants.py
info_label = QLabel( info_label = QLabel(
f"YTSage requires yt-dlp to download videos.<br><br>" _("ytdlp_setup.description", os_name=OS_FULL_NAME)
f"yt-dlp was not found in the app's local directory. "
f"YTSage needs to set up yt-dlp for your {OS_FULL_NAME} system.<br><br>"
f"Please choose an option below:"
) )
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
info_label.setWordWrap(True) info_label.setWordWrap(True)
@@ -278,9 +276,9 @@ class YtdlpSetupDialog(QDialog):
option_layout.setSpacing(8) option_layout.setSpacing(8)
option_layout.setContentsMargins(0, 0, 0, 0) option_layout.setContentsMargins(0, 0, 0, 0)
self.auto_radio = QRadioButton("Download automatically (Recommended)") self.auto_radio = QRadioButton(_("ytdlp_setup.option_auto"))
self.auto_radio.setChecked(True) self.auto_radio.setChecked(True)
self.manual_radio = QRadioButton("Select path manually") self.manual_radio = QRadioButton(_("ytdlp_setup.option_manual"))
option_layout.addWidget(self.auto_radio) option_layout.addWidget(self.auto_radio)
option_layout.addWidget(self.manual_radio) option_layout.addWidget(self.manual_radio)
@@ -326,10 +324,10 @@ class YtdlpSetupDialog(QDialog):
button_layout.setSpacing(15) button_layout.setSpacing(15)
button_layout.setContentsMargins(0, 10, 0, 0) # Add top margin for buttons button_layout.setContentsMargins(0, 10, 0, 0) # Add top margin for buttons
self.setup_button = QPushButton("Setup yt-dlp") self.setup_button = QPushButton(_("ytdlp_setup.setup_button"))
self.setup_button.clicked.connect(self.setup_ytdlp) self.setup_button.clicked.connect(self.setup_ytdlp)
self.cancel_button = QPushButton("Cancel") self.cancel_button = QPushButton(_("buttons.cancel"))
self.cancel_button.clicked.connect(self.reject) self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.setup_button) button_layout.addWidget(self.setup_button)
@@ -347,7 +345,7 @@ class YtdlpSetupDialog(QDialog):
def download_ytdlp(self) -> None: def download_ytdlp(self) -> None:
self.progress_bar.setVisible(True) self.progress_bar.setVisible(True)
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.status_label.setText("Downloading yt-dlp...") self.status_label.setText(_("ytdlp_setup.downloading"))
self.setup_button.setEnabled(False) self.setup_button.setEnabled(False)
self.cancel_button.setEnabled(False) self.cancel_button.setEnabled(False)
@@ -364,15 +362,15 @@ class YtdlpSetupDialog(QDialog):
self.cancel_button.setEnabled(True) self.cancel_button.setEnabled(True)
if success: if success:
self.status_label.setText("yt-dlp was successfully installed!") self.status_label.setText(_("ytdlp_setup.success"))
self.setup_complete.emit(result) self.setup_complete.emit(result)
self.accept() self.accept()
else: else:
self.status_label.setText(f"Error: {result}") self.status_label.setText(_("ytdlp_setup.error", error=result))
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical) error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Download Failed") error_dialog.setWindowTitle(_("ytdlp_setup.download_failed_title"))
error_dialog.setText(f"Failed to download yt-dlp: {result}") error_dialog.setText(_("ytdlp_setup.download_failed_message", error=result))
# Set the window icon to match the main dialog # Set the window icon to match the main dialog
error_dialog.setWindowIcon(self.windowIcon()) error_dialog.setWindowIcon(self.windowIcon())
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
@@ -401,9 +399,9 @@ class YtdlpSetupDialog(QDialog):
def select_ytdlp_path(self) -> None: def select_ytdlp_path(self) -> None:
if OS_NAME == "Windows": if OS_NAME == "Windows":
file_filter = "Executable Files (*.exe)" file_filter = _("ytdlp_setup.file_filter_windows")
else: else:
file_filter = "All Files (*)" file_filter = _("ytdlp_setup.file_filter_all")
# Apply style to QFileDialog # Apply style to QFileDialog
file_dialog = QFileDialog(self) file_dialog = QFileDialog(self)
@@ -431,7 +429,9 @@ class YtdlpSetupDialog(QDialog):
""" """
) )
file_path, _ = file_dialog.getOpenFileName(self, "Select yt-dlp executable", "", file_filter) file_path, _ = file_dialog.getOpenFileName(
self, _("ytdlp_setup.select_executable_title"), "", file_filter
)
if file_path: if file_path:
logger.debug(f"User selected file: {file_path}") logger.debug(f"User selected file: {file_path}")
@@ -465,7 +465,7 @@ class YtdlpSetupDialog(QDialog):
logger.debug(f"Permissions set on Unix system") logger.debug(f"Permissions set on Unix system")
# Return the path of the copied file # Return the path of the copied file
self.status_label.setText(f"yt-dlp successfully copied to {target_path}") self.status_label.setText(_("ytdlp_setup.copied_to", path=target_path))
logger.debug(f"Emitting setup_complete signal with path: {target_path}") logger.debug(f"Emitting setup_complete signal with path: {target_path}")
self.setup_complete.emit(target_path) self.setup_complete.emit(target_path)
self.accept() self.accept()
@@ -473,8 +473,8 @@ class YtdlpSetupDialog(QDialog):
logger.debug(f"Error copying file: {copy_error}", exc_info=True) logger.debug(f"Error copying file: {copy_error}", exc_info=True)
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical) error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Setup Error") error_dialog.setWindowTitle(_("ytdlp_setup.setup_error_title"))
error_dialog.setText(f"Error copying yt-dlp to app directory: {copy_error}") error_dialog.setText(_("ytdlp_setup.copy_error", error=copy_error))
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
""" """
QMessageBox { QMessageBox {
@@ -502,8 +502,8 @@ class YtdlpSetupDialog(QDialog):
logger.debug(f"File verification failed with return code: {result.returncode}") logger.debug(f"File verification failed with return code: {result.returncode}")
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Warning) error_dialog.setIcon(QMessageBox.Icon.Warning)
error_dialog.setWindowTitle("Invalid Executable") error_dialog.setWindowTitle(_("ytdlp_setup.invalid_executable_title"))
error_dialog.setText("The selected file does not appear to be a valid yt-dlp executable.") error_dialog.setText(_("ytdlp_setup.invalid_executable_message"))
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
""" """
QMessageBox { QMessageBox {
@@ -531,8 +531,8 @@ class YtdlpSetupDialog(QDialog):
logger.debug(f"Exception during verification: {e}", exc_info=True) logger.debug(f"Exception during verification: {e}", exc_info=True)
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical) error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Error") error_dialog.setWindowTitle(_("main_ui.error_title"))
error_dialog.setText(f"Error verifying yt-dlp executable: {e}") error_dialog.setText(_("ytdlp_setup.verify_error", error=e))
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
""" """
QMessageBox { QMessageBox {
@@ -678,8 +678,8 @@ def setup_ytdlp(parent_widget=None):
if parent_widget: if parent_widget:
error_dialog = QMessageBox(parent_widget) error_dialog = QMessageBox(parent_widget)
error_dialog.setIcon(QMessageBox.Icon.Warning) error_dialog.setIcon(QMessageBox.Icon.Warning)
error_dialog.setWindowTitle("Setup Failed") error_dialog.setWindowTitle(_("ytdlp_setup.setup_failed_title"))
error_dialog.setText("Failed to set up yt-dlp. Some features may not work correctly.") error_dialog.setText(_("ytdlp_setup.setup_failed_message"))
# Set the window icon to match the parent # Set the window icon to match the parent
error_dialog.setWindowIcon(parent_widget.windowIcon()) error_dialog.setWindowIcon(parent_widget.windowIcon())
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
@@ -712,3 +712,43 @@ def setup_ytdlp(parent_widget=None):
# User cancelled or setup failed, return the fallback command # User cancelled or setup failed, return the fallback command
logger.debug("Returning fallback command 'yt-dlp'") logger.debug("Returning fallback command 'yt-dlp'")
return "yt-dlp" return "yt-dlp"
def check_ytdlp_deno_integration() -> bool:
"""
Check if yt-dlp is integrated with Deno by running 'yt-dlp --verbose'.
Returns:
bool: True if Deno is detected in JS runtimes, False otherwise
"""
try:
ytdlp_path = get_yt_dlp_path()
if not ytdlp_path or ytdlp_path == "yt-dlp":
return False
# Run yt-dlp --verbose to check JS runtimes
# We use a dummy URL or just --verbose with no URL (which might error but should print debug info)
# However, yt-dlp might not print debug info if no URL is provided and it errors out immediately with "usage".
# But per user example: "yt-dlp.exe: error: You must provide at least one URL." comes AFTER debug info.
result = subprocess.run(
[str(ytdlp_path), "--verbose"],
capture_output=True,
text=True,
timeout=10,
creationflags=SUBPROCESS_CREATIONFLAGS
)
# Check stderr for "[debug] JS runtimes: deno"
output = result.stderr
if "[debug] JS runtimes:" in output and "deno" in output:
# Find the line
for line in output.splitlines():
if "[debug] JS runtimes:" in line and "deno" in line:
logger.info(f"Deno integration detected: {line.strip()}")
return True
return False
except Exception as e:
logger.warning(f"Failed to check yt-dlp Deno integration: {e}")
return False
+435
View File
@@ -0,0 +1,435 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
import json
import subprocess
from PySide6.QtCore import QMetaObject, Qt, Q_ARG, QThread, Signal, QTimer
from PySide6.QtWidgets import QMessageBox
from ..core.ytsage_utils import validate_video_url, parse_yt_dlp_error
from ..core.ytsage_yt_dlp import get_yt_dlp_path
from ..utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS
from ..utils.ytsage_localization import _
from ..utils.ytsage_logger import logger
if TYPE_CHECKING:
from .ytsage_gui_main import YTSageApp
class AnalysisThread(QThread):
"""
Thread-safe QThread for URL analysis.
All results are passed back via signals to ensure thread safety.
"""
# Signals for status updates
status_update = Signal(str)
progress_update = Signal(int) # Signal for progress bar updates
# Signals for playlist UI
playlist_info_visible = Signal(bool)
playlist_info_text = Signal(str)
playlist_select_btn_visible = Signal(bool)
playlist_select_btn_text = Signal(str)
# Signal for analysis results - passes all data at once
analysis_complete = Signal(dict)
# Signal for errors
analysis_error = Signal(str)
# Signal when thread finishes (success or failure)
analysis_finished = Signal()
def __init__(
self,
url: str,
cookie_file_path: Optional[str] = None,
browser_cookies_option: Optional[str] = None,
proxy_url: Optional[str] = None,
geo_proxy_url: Optional[str] = None,
parent=None
) -> None:
super().__init__(parent)
self.url = url
self.cookie_file_path = cookie_file_path
self.browser_cookies_option = browser_cookies_option
self.proxy_url = proxy_url
self.geo_proxy_url = geo_proxy_url
self._cancelled = False
def cancel(self) -> None:
"""Request cancellation of the analysis."""
self._cancelled = True
def run(self) -> None:
"""Main thread execution - performs URL analysis."""
try:
self.status_update.emit(_("main_ui.analyzing_extracting_basic"))
self.progress_update.emit(15)
url = self.url
# Clean up the URL to handle both playlist and video URLs
if "list=" in url and "watch?v=" in url:
playlist_id = url.split("list=")[1].split("&")[0]
url = f"https://www.youtube.com/playlist?list={playlist_id}"
self._analyze_url_with_subprocess(url)
except Exception as e:
logger.exception(f"Error in analysis: {e}")
self.analysis_error.emit(_("errors.generic_error", error=str(e)))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
finally:
self.analysis_finished.emit()
def _add_auth_options(self, cmd: List[str]) -> None:
"""Add authentication and proxy options to command."""
if self.cookie_file_path:
cmd.extend(["--cookies", str(self.cookie_file_path)])
elif self.browser_cookies_option:
cmd.extend(["--cookies-from-browser", self.browser_cookies_option])
if self.proxy_url:
cmd.extend(["--proxy", self.proxy_url])
if self.geo_proxy_url:
cmd.extend(["--geo-verification-proxy", self.geo_proxy_url])
def _analyze_url_with_subprocess(self, url: str) -> None:
"""Analyze URL using yt-dlp executable."""
if self._cancelled:
return
yt_dlp_path = get_yt_dlp_path()
if not yt_dlp_path:
logger.error("yt-dlp executable not found. Please install yt-dlp first.")
self.analysis_error.emit(_("errors.ytdlp_not_found"))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
return
self.status_update.emit(_("main_ui.analyzing_extracting_ytdlp"))
self.progress_update.emit(30) # This will trigger fake progress in UI
# Build command for basic info extraction
cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url]
self._add_auth_options(cmd)
logger.debug(f"Executing yt-dlp command: {cmd}")
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=300,
creationflags=SUBPROCESS_CREATIONFLAGS
)
except subprocess.TimeoutExpired:
logger.error("Analysis timed out")
self.analysis_error.emit(_("errors.timeout"))
return
if self._cancelled:
return
if result.returncode != 0:
logger.error(f"yt-dlp failed: {result.stderr}")
self.analysis_error.emit(_("errors.ytdlp_failed", error=result.stderr))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
return
json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
if not json_lines:
logger.error("No data returned from yt-dlp")
self.analysis_error.emit(_("errors.no_data_returned"))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
return
try:
first_info = json.loads(json_lines[0])
except json.JSONDecodeError as e:
logger.error(f"Failed to parse yt-dlp output: {e}")
self.analysis_error.emit(_("errors.parse_failed", error=str(e)))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
return
if self._cancelled:
return
self.status_update.emit(_("main_ui.analyzing_processing_data"))
self.progress_update.emit(60)
# Prepare result data
result_data: Dict[str, Any] = {
"is_playlist": False,
"playlist_info": None,
"playlist_entries": [],
"video_info": None,
"all_formats": [],
"available_subtitles": {},
"available_automatic_subtitles": {},
"thumbnail_url": None,
}
if first_info.get("_type") == "playlist":
result_data["is_playlist"] = True
result_data["playlist_info"] = first_info
playlist_entries = first_info.get("entries", [])
result_data["playlist_entries"] = playlist_entries
if not playlist_entries:
logger.error("Playlist contains no valid videos.")
self.analysis_error.emit(_("errors.playlist_no_videos"))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
return
# Fetch full info for the first video to get formats
self.status_update.emit(_("main_ui.analyzing_fetching_first_video"))
self.progress_update.emit(70)
first_video_entry = playlist_entries[0]
first_video_url = first_video_entry.get("url")
cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url]
self._add_auth_options(cmd_single)
try:
result_single = subprocess.run(
cmd_single, capture_output=True, text=True, timeout=60,
creationflags=SUBPROCESS_CREATIONFLAGS
)
if result_single.returncode == 0:
result_data["video_info"] = json.loads(result_single.stdout)
else:
result_data["video_info"] = first_video_entry
except subprocess.TimeoutExpired:
result_data["video_info"] = first_video_entry
if self._cancelled:
return
# Update playlist UI via signals
playlist_text = _("playlist.display_format",
title=first_info.get('title', _('playlist.unknown')),
count=len(playlist_entries))
self.playlist_info_text.emit(playlist_text)
self.playlist_info_visible.emit(True)
self.playlist_select_btn_text.emit(_("main_ui.select_videos_all"))
self.playlist_select_btn_visible.emit(True)
else:
# Handle single video
result_data["is_playlist"] = False
result_data["video_info"] = first_info
result_data["playlist_entries"] = []
result_data["playlist_info"] = None
# Hide playlist UI
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
# Verify we have format information
video_info = result_data["video_info"]
if not video_info or "formats" not in video_info:
logger.error("No format information available")
self.analysis_error.emit(_("errors.no_format_info"))
self.playlist_info_visible.emit(False)
self.playlist_select_btn_visible.emit(False)
return
self.status_update.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
self.progress_update.emit(85)
result_data["all_formats"] = video_info.get("formats", [])
# Get thumbnail URL
self.status_update.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
self.progress_update.emit(90)
playlist_info = result_data.get("playlist_info") or {}
thumbnail_url = playlist_info.get("thumbnail") or video_info.get("thumbnail")
result_data["thumbnail_url"] = thumbnail_url
# Handle subtitles
self.status_update.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
self.progress_update.emit(92)
result_data["available_subtitles"] = video_info.get("subtitles", {})
result_data["available_automatic_subtitles"] = video_info.get("automatic_captions", {})
self.status_update.emit(_("main_ui.analyzing_updating_table"))
self.progress_update.emit(95)
# Emit all results at once
self.analysis_complete.emit(result_data)
class AnalysisMixin:
"""Mixin class providing URL analysis functionality for YTSageApp."""
# Track the current analysis thread
_analysis_thread: Optional[AnalysisThread] = None
_analysis_timer: Optional[QTimer] = None
_fake_progress: int = 0
def analyze_url(self) -> None:
"""Start URL analysis in a background thread."""
self = cast("YTSageApp", self)
if self.is_updating_ytdlp:
QMessageBox.warning(self, _("update.update_in_progress_title"), _("update.update_in_progress_message"))
return
url = self.url_input.text().strip()
if not url:
self.signals.update_status.emit(_("main_ui.invalid_url_or_enter"))
if hasattr(self, "animate_widget_shake"):
self.animate_widget_shake(self.url_input)
return
# Validate URL before processing
is_valid, error_message = validate_video_url(url)
if not is_valid:
QMessageBox.warning(self, _("main_ui.error_title"), error_message)
if hasattr(self, "animate_widget_shake"):
self.animate_widget_shake(self.url_input)
return
# Cancel any existing analysis thread
if self._analysis_thread is not None and self._analysis_thread.isRunning():
self._analysis_thread.cancel()
self._analysis_thread.wait(1000) # Wait up to 1 second
# Reset analysis state and disable controls
self.analysis_completed = False
self.toggle_analysis_dependent_controls(enabled=False)
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
self.signals.update_progress.emit(0) # Reset progress
self.is_analyzing = True
# Stop any existing timer
if self._analysis_timer:
self._analysis_timer.stop()
self._analysis_timer = None
# Create and configure the analysis thread
self._analysis_thread = AnalysisThread(
url=url,
# ... arguments will be filled by ... usage below ...
cookie_file_path=self.cookie_file_path,
browser_cookies_option=self.browser_cookies_option,
proxy_url=self.proxy_url,
geo_proxy_url=self.geo_proxy_url,
parent=self
)
# Connect signals to handlers
self._analysis_thread.status_update.connect(self.signals.update_status.emit)
self._analysis_thread.progress_update.connect(self._handle_analysis_progress)
self._analysis_thread.playlist_info_visible.connect(self.signals.playlist_info_label_visible.emit)
self._analysis_thread.playlist_info_text.connect(self.signals.playlist_info_label_text.emit)
self._analysis_thread.playlist_select_btn_visible.connect(self.signals.playlist_select_btn_visible.emit)
self._analysis_thread.playlist_select_btn_text.connect(self.signals.playlist_select_btn_text.emit)
self._analysis_thread.analysis_complete.connect(self._on_analysis_complete)
self._analysis_thread.analysis_error.connect(self._on_analysis_error)
self._analysis_thread.analysis_finished.connect(self._on_analysis_finished)
# Start the thread
self._analysis_thread.start()
def _handle_analysis_progress(self, value: int) -> None:
"""Handle progress updates from analysis thread."""
self = cast("YTSageApp", self)
# Stop fake timer if running on any real update
if self._analysis_timer:
self._analysis_timer.stop()
self._analysis_timer = None
self.signals.update_progress.emit(value)
# If we hit the extraction phase (30%), start fake progress
if value == 30:
self._fake_progress = 30
self._analysis_timer = QTimer(self)
self._analysis_timer.timeout.connect(self._update_fake_progress)
self._analysis_timer.start(200) # Every 200ms
def _update_fake_progress(self) -> None:
"""Increment progress bar slowly during long operations."""
self = cast("YTSageApp", self)
# Asymptotically approach 85%
if self._fake_progress < 85:
# Slow down as we get higher
increment = 1
if self._fake_progress > 60:
if self._fake_progress % 3 == 0: # Slower
increment = 1
else:
increment = 0
if increment > 0:
self._fake_progress += increment
self.signals.update_progress.emit(self._fake_progress)
def _on_analysis_complete(self, result_data: Dict[str, Any]) -> None:
"""Handle successful analysis completion - runs in main thread."""
self = cast("YTSageApp", self)
# Stop fake timer
if self._analysis_timer:
self._analysis_timer.stop()
self._analysis_timer = None
self.signals.update_progress.emit(100)
# Update instance variables with results (safe - we're in main thread)
self.is_playlist = result_data["is_playlist"]
self.playlist_info = result_data["playlist_info"]
self.playlist_entries = result_data["playlist_entries"]
self.video_info = result_data["video_info"]
self.available_subtitles = result_data["available_subtitles"]
self.available_automatic_subtitles = result_data["available_automatic_subtitles"]
self.selected_playlist_items = None
self.selected_subtitles = []
# Update UI components (safe - we're in main thread)
self.update_video_info(self.video_info)
# Download thumbnail
thumbnail_url = result_data.get("thumbnail_url")
if thumbnail_url:
self.download_thumbnail(thumbnail_url)
# Save thumbnail if enabled
if self.save_thumbnail:
self.download_thumbnail_file(self.video_url, self.last_path)
# Update subtitle UI
self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected"))
# Update format table
self.video_button.setChecked(True)
self.audio_button.setChecked(False)
self.update_format_table(result_data["all_formats"])
self.signals.update_status.emit(_("main_ui.analysis_complete"))
# Mark analysis as complete and enable controls
self.analysis_completed = True
self.toggle_analysis_dependent_controls(enabled=True)
def _on_analysis_error(self, error_message: str) -> None:
"""Handle analysis error - runs in main thread."""
self = cast("YTSageApp", self)
if self._analysis_timer:
self._analysis_timer.stop()
self._analysis_timer = None
self.signals.update_progress.emit(0)
self.signals.update_status.emit(error_message)
def _on_analysis_finished(self) -> None:
"""Handle analysis thread completion - runs in main thread."""
self = cast("YTSageApp", self)
self.is_analyzing = False
@@ -12,18 +12,18 @@ This package contains all dialog classes organized by functionality:
""" """
# Re-export all dialog classes for backward compatibility # Re-export all dialog classes for backward compatibility
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow from .ytsage_dialogs_base import AboutDialog, LogWindow
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog from .ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread from .ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_history import HistoryDialog from .ytsage_dialogs_history import HistoryDialog
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import ( from .ytsage_dialogs_selection import (
PlaylistSelectionDialog, PlaylistSelectionDialog,
SponsorBlockCategoryDialog, SponsorBlockCategoryDialog,
SubtitleSelectionDialog, SubtitleSelectionDialog,
) )
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog from .ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog from .ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget from .ytsage_dialogs_updater import UpdaterTabWidget
__all__ = [ __all__ = [
# Base dialogs # Base dialogs
@@ -5,7 +5,8 @@ Contains basic utility dialogs like LogWindow and AboutDialog.
from datetime import datetime from datetime import datetime
from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtCore import Qt, QThread, QTimer, Signal, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
@@ -19,19 +20,66 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src import __version__ as APP_VERSION from ... import __version__ as APP_VERSION
from src.utils.ytsage_localization import _ from ...utils.ytsage_localization import _
from ...utils.ytsage_logger import logger
from ...utils.ytsage_constants import APP_LOG_DIR
from src.core.ytsage_ffmpeg import get_ffmpeg_path from ...core.ytsage_ffmpeg import get_ffmpeg_path
from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache from ...core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache
from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path from ...core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path, check_ytdlp_deno_integration
from src.core.ytsage_deno import check_deno_installed, get_deno_path from ...core.ytsage_deno import check_deno_installed, get_deno_path
class SystemInfoThread(QThread):
"""Background thread to gather system information."""
info_ready = Signal(dict)
def run(self):
info = {}
# yt-dlp Status
ytdlp_found = check_ytdlp_installed()
info['ytdlp_found'] = ytdlp_found
info['ytdlp_version'] = get_ytdlp_version()
info['ytdlp_path'] = get_yt_dlp_path() if ytdlp_found else None
# yt-dlp cache status
ytdlp_cache = _version_cache.get("ytdlp", {})
info['ytdlp_last_check'] = ytdlp_cache.get("last_check", 0)
# FFmpeg Status
ffmpeg_found = check_ffmpeg()
info['ffmpeg_found'] = ffmpeg_found
info['ffmpeg_version'] = get_ffmpeg_version() if ffmpeg_found else _('about.not_available')
info['ffmpeg_path'] = get_ffmpeg_path() if ffmpeg_found else None
# FFmpeg cache status
ffmpeg_cache = _version_cache.get("ffmpeg", {})
info['ffmpeg_last_check'] = ffmpeg_cache.get("last_check", 0)
# Deno Status
deno_found = check_deno_installed()
info['deno_found'] = deno_found
info['deno_version'] = get_deno_version() if deno_found else _('about.not_available')
info['deno_path'] = get_deno_path() if deno_found else None
# Deno cache status
deno_cache = _version_cache.get("deno", {})
info['deno_last_check'] = deno_cache.get("last_check", 0)
# Check integration with yt-dlp if both are present
info['integration_status'] = False
if deno_found and ytdlp_found:
info['integration_status'] = check_ytdlp_deno_integration()
self.info_ready.emit(info)
class LogWindow(QDialog): class LogWindow(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("yt-dlp Log") self.setWindowTitle(_("dialogs.ytdlp_log_title"))
self.setMinimumSize(700, 500) self.setMinimumSize(700, 500)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@@ -178,18 +226,25 @@ class AboutDialog(QDialog):
info_layout = QHBoxLayout() info_layout = QHBoxLayout()
info_layout.setSpacing(15) info_layout.setSpacing(15)
author_link = '<a href="https://github.com/oop7/" style="color: #c90000; text-decoration: none;">oop7</a>'
author_label = QLabel( author_label = QLabel(
f"{_('about.author', author='<a href=\'https://github.com/oop7/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>oop7</a>')}" f"{_('about.author', author=author_link)}"
) )
author_label.setOpenExternalLinks(True) author_label.setOpenExternalLinks(True)
info_layout.addWidget(author_label) info_layout.addWidget(author_label)
repo_link = '<a href="https://github.com/oop7/YTSage/" style="color: #c90000; text-decoration: none;">YTSage</a>'
repo_label = QLabel( repo_label = QLabel(
f"{_('about.github', repo='<a href=\'https://github.com/oop7/YTSage/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>YTSage</a>')}" f"{_('about.github', repo=repo_link)}"
) )
repo_label.setOpenExternalLinks(True) repo_label.setOpenExternalLinks(True)
info_layout.addWidget(repo_label) info_layout.addWidget(repo_label)
sponsor_link = '<a href="https://github.com/sponsors/oop7" style="color: #c90000; text-decoration: none;">❤️ Sponsor</a>'
sponsor_label = QLabel(sponsor_link)
sponsor_label.setOpenExternalLinks(True)
info_layout.addWidget(sponsor_label)
# Center the info layout # Center the info layout
info_container = QHBoxLayout() info_container = QHBoxLayout()
info_container.addStretch() info_container.addStretch()
@@ -241,6 +296,34 @@ class AboutDialog(QDialog):
# Add stretch to push refresh button to the right # Add stretch to push refresh button to the right
header_layout.addStretch() header_layout.addStretch()
# Create logs button (minimal)
self.logs_btn = QPushButton(_("about.open_logs")) # Expected to be small text or icon
self.logs_btn.setToolTip(_("about.logs_tooltip"))
self.logs_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.logs_btn.setStyleSheet(
"""
QPushButton {
padding: 1px 6px;
background-color: transparent;
border: 1px solid #333;
border-radius: 4px;
color: #888888;
font-size: 10px;
margin-right: 8px;
}
QPushButton:hover {
color: #ffffff;
border-color: #555;
background-color: rgba(255, 255, 255, 0.05);
}
QPushButton:pressed {
background-color: rgba(255, 255, 255, 0.1);
}
"""
)
self.logs_btn.clicked.connect(self.open_logs_folder)
header_layout.addWidget(self.logs_btn)
# Create refresh button # Create refresh button
self.refresh_btn = QPushButton(_("about.refresh")) self.refresh_btn = QPushButton(_("about.refresh"))
self.refresh_btn.setFixedSize(16, 16) self.refresh_btn.setFixedSize(16, 16)
@@ -288,14 +371,15 @@ class AboutDialog(QDialog):
def _show_loading_message(self) -> None: def _show_loading_message(self) -> None:
"""Show a compact loading message while system information is being gathered.""" """Show a compact loading message while system information is being gathered."""
loading_label = QLabel(_("about.loading")) # Strip the emoji from the localized string to avoid rendering issues
loading_text = _("about.loading").replace("🔄", "").strip()
loading_label = QLabel(loading_text)
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
loading_label.setStyleSheet( loading_label.setStyleSheet(
""" """
QLabel { QLabel {
color: #cccccc; color: #888888;
font-size: 11px; font-size: 11px;
font-style: italic;
padding: 10px; padding: 10px;
} }
""" """
@@ -376,7 +460,13 @@ class AboutDialog(QDialog):
return item_widget return item_widget
def update_system_info(self) -> None: def update_system_info(self) -> None:
"""Update the system information display with compact layout.""" """Start background thread to gather system info."""
self.info_thread = SystemInfoThread()
self.info_thread.info_ready.connect(self._populate_system_info)
self.info_thread.start()
def _populate_system_info(self, info: dict) -> None:
"""Populate UI with gathered info."""
# Clear existing items # Clear existing items
for i in reversed(range(self.status_container.count())): for i in reversed(range(self.status_container.count())):
child = self.status_container.itemAt(i).widget() child = self.status_container.itemAt(i).widget()
@@ -384,19 +474,18 @@ class AboutDialog(QDialog):
child.deleteLater() child.deleteLater()
# yt-dlp Status - compact version with path # yt-dlp Status - compact version with path
ytdlp_found = check_ytdlp_installed() ytdlp_found = info['ytdlp_found']
ytdlp_status_text = ( ytdlp_status_text = (
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if ytdlp_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>" f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if ytdlp_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
) )
ytdlp_version = get_ytdlp_version() ytdlp_version = info['ytdlp_version']
# Get yt-dlp path # Get yt-dlp path
ytdlp_path = get_yt_dlp_path() if ytdlp_found else None ytdlp_path = info['ytdlp_path']
ytdlp_path_text = ytdlp_path if ytdlp_path and ytdlp_path != "yt-dlp" else None ytdlp_path_text = ytdlp_path if ytdlp_path and ytdlp_path != "yt-dlp" else None
# Simplified cache status # Simplified cache status
ytdlp_cache = _version_cache.get("ytdlp", {}) last_check = info['ytdlp_last_check']
last_check = ytdlp_cache.get("last_check", 0)
cache_status = "" cache_status = ""
if last_check > 0: if last_check > 0:
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
@@ -412,23 +501,19 @@ class AboutDialog(QDialog):
self.status_container.addWidget(ytdlp_item) self.status_container.addWidget(ytdlp_item)
# FFmpeg Status - compact version with path # FFmpeg Status - compact version with path
ffmpeg_found = check_ffmpeg() ffmpeg_found = info['ffmpeg_found']
ffmpeg_status_text = ( ffmpeg_status_text = (
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
if ffmpeg_found if ffmpeg_found
else f"<span style='color: #F44336;'>{_('about.missing')}</span>" else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
) )
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else _('about.not_available') ffmpeg_version = info['ffmpeg_version']
# Get FFmpeg path # Get FFmpeg path
ffmpeg_path_text = None ffmpeg_path_text = info['ffmpeg_path']
if ffmpeg_found:
ffmpeg_path = get_ffmpeg_path()
ffmpeg_path_text = ffmpeg_path if ffmpeg_path else None
# Simplified cache status for FFmpeg # Simplified cache status for FFmpeg
ffmpeg_cache = _version_cache.get("ffmpeg", {}) last_check = info['ffmpeg_last_check']
last_check = ffmpeg_cache.get("last_check", 0)
cache_status = "" cache_status = ""
if last_check > 0 and ffmpeg_found: if last_check > 0 and ffmpeg_found:
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
@@ -444,41 +529,60 @@ class AboutDialog(QDialog):
self.status_container.addWidget(ffmpeg_item) self.status_container.addWidget(ffmpeg_item)
# Deno Status - compact version with path (only show path if in app bin directory) # Deno Status - compact version with path (only show path if in app bin directory)
deno_found = check_deno_installed() deno_found = info['deno_found']
deno_status_text = ( deno_status_text = (
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if deno_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>" f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if deno_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
) )
deno_version = get_deno_version() if deno_found else _('about.not_available') deno_version = info['deno_version']
# Get Deno path - only show if in app bin directory # Get Deno path - only show if in app bin directory
deno_path_text = None deno_path_text = None
if deno_found: if deno_found:
deno_path = get_deno_path() deno_path = info['deno_path']
# Only show path if it's not the fallback "deno" and the file exists # Only show path if it's not the fallback "deno" and the file exists
if deno_path and deno_path != "deno": if deno_path and deno_path != "deno":
from pathlib import Path from pathlib import Path
from src.utils.ytsage_constants import DENO_APP_BIN_PATH from ...utils.ytsage_constants import DENO_APP_BIN_PATH
# Check if the path is our managed binary # Check if the path is our managed binary
if Path(deno_path).resolve() == DENO_APP_BIN_PATH.resolve(): if Path(deno_path).resolve() == DENO_APP_BIN_PATH.resolve():
deno_path_text = deno_path deno_path_text = deno_path
# Simplified cache status for Deno # Simplified cache status for Deno
deno_cache = _version_cache.get("deno", {}) last_check = info['deno_last_check']
last_check = deno_cache.get("last_check", 0)
cache_status = "" cache_status = ""
if last_check > 0 and deno_found: if last_check > 0 and deno_found:
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>"
# Check integration with yt-dlp if both are present
integration_status = ""
if info.get('integration_status', False):
integration_status = f" <span style='color: #4CAF50; font-size: 10px; font-weight: bold;'> + yt-dlp</span>"
deno_item = self._create_status_item( deno_item = self._create_status_item(
"🦕", "🦕",
"Deno", "Deno",
deno_status_text, deno_status_text,
deno_version + cache_status, deno_version + cache_status + integration_status,
deno_path_text, deno_path_text,
) )
self.status_container.addWidget(deno_item) self.status_container.addWidget(deno_item)
def open_logs_folder(self):
"""Open the application logs folder in the system file explorer."""
try:
if not APP_LOG_DIR.exists():
logger.warning(f"Log directory does not exist: {APP_LOG_DIR}")
APP_LOG_DIR.mkdir(parents=True, exist_ok=True)
log_url = QUrl.fromLocalFile(str(APP_LOG_DIR))
QDesktopServices.openUrl(log_url)
except Exception as e:
logger.error(f"Failed to open log folder: {e}")
# Minimal error feedback since this is about dialog
self.logs_btn.setToolTip(f"Error: {str(e)}")
def refresh_version_info(self) -> None: def refresh_version_info(self) -> None:
"""Refresh version information manually.""" """Refresh version information manually."""
self.refresh_btn.setText(_('about.refreshing')) self.refresh_btn.setText(_('about.refreshing'))
@@ -29,16 +29,17 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.core.ytsage_yt_dlp import get_yt_dlp_path from ..ytsage_smooth_tab_widget import SmoothTabWidget
from src.core.ytsage_utils import update_auto_update_settings from ...core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import YTDLP_DOCS_URL from ...core.ytsage_utils import update_auto_update_settings
from src.utils.ytsage_config_manager import ConfigManager from ...utils.ytsage_constants import YTDLP_DOCS_URL
from src.utils.ytsage_localization import LocalizationManager, _ from ...utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_logger import logger from ...utils.ytsage_localization import LocalizationManager, _
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget from ...utils.ytsage_logger import logger
from .ytsage_dialogs_updater import UpdaterTabWidget
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import) from ..ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
class CommandWorker(QObject): class CommandWorker(QObject):
@@ -115,7 +116,7 @@ class CustomOptionsDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Create tab widget to organize content # Create tab widget to organize content
self.tab_widget = QTabWidget() self.tab_widget = SmoothTabWidget()
layout.addWidget(self.tab_widget) layout.addWidget(self.tab_widget)
# === Cookies Tab === # === Cookies Tab ===
@@ -542,7 +543,7 @@ class CustomOptionsDialog(QDialog):
QDialog { QDialog {
background-color: #15181b; background-color: #15181b;
} }
QTabWidget::pane { QFrame#tabContent {
border: 1px solid #3d3d3d; border: 1px solid #3d3d3d;
background-color: #15181b; background-color: #15181b;
} }
@@ -675,13 +676,17 @@ class CustomOptionsDialog(QDialog):
def _update_cookies_active_status(self) -> None: def _update_cookies_active_status(self) -> None:
"""Update the status indicator showing if cookies are currently active""" """Update the status indicator showing if cookies are currently active"""
if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option: if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option:
self.cookies_active_status.setText(f"✓ Active: Browser cookies ({self._parent.browser_cookies_option})") self.cookies_active_status.setText(
_("cookies.active_browser", browser=self._parent.browser_cookies_option)
)
self.cookies_active_status.setStyleSheet("color: #00cc00; font-weight: bold;") self.cookies_active_status.setStyleSheet("color: #00cc00; font-weight: bold;")
elif hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path: elif hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
self.cookies_active_status.setText(f"✓ Active: Cookie file ({self._parent.cookie_file_path.name})") self.cookies_active_status.setText(
_("cookies.active_file", file=self._parent.cookie_file_path.name)
)
self.cookies_active_status.setStyleSheet("color: #00cc00; font-weight: bold;") self.cookies_active_status.setStyleSheet("color: #00cc00; font-weight: bold;")
else: else:
self.cookies_active_status.setText("○ No cookies active") self.cookies_active_status.setText(_("cookies.none_active"))
self.cookies_active_status.setStyleSheet("color: #888888; font-style: italic;") self.cookies_active_status.setStyleSheet("color: #888888; font-style: italic;")
def _initialize_proxy_settings(self) -> None: def _initialize_proxy_settings(self) -> None:
@@ -854,9 +859,9 @@ class CustomOptionsDialog(QDialog):
if saved_main or saved_geo: if saved_main or saved_geo:
status_parts = [] status_parts = []
if saved_main: if saved_main:
status_parts.append(f"Saved main proxy: {saved_main}") status_parts.append(_("proxy.saved_main", proxy=saved_main))
if saved_geo: if saved_geo:
status_parts.append(f"Saved geo proxy: {saved_geo}") status_parts.append(_("proxy.saved_geo", proxy=saved_geo))
self.proxy_status.setText(" | ".join(status_parts)) self.proxy_status.setText(" | ".join(status_parts))
self.proxy_status.setStyleSheet("color: #888888; font-style: italic;") self.proxy_status.setStyleSheet("color: #888888; font-style: italic;")
else: else:
@@ -866,10 +871,10 @@ class CustomOptionsDialog(QDialog):
issues = [] issues = []
if main_proxy and not self.validate_proxy_url(main_proxy): if main_proxy and not self.validate_proxy_url(main_proxy):
issues.append("Invalid main proxy URL format") issues.append(_("proxy.invalid_main_url"))
if geo_proxy and not self.validate_proxy_url(geo_proxy): if geo_proxy and not self.validate_proxy_url(geo_proxy):
issues.append("Invalid geo proxy URL format") issues.append(_("proxy.invalid_geo_url"))
if issues: if issues:
self.proxy_status.setText(" | ".join(issues)) self.proxy_status.setText(" | ".join(issues))
@@ -877,9 +882,9 @@ class CustomOptionsDialog(QDialog):
else: else:
status_parts = [] status_parts = []
if main_proxy: if main_proxy:
status_parts.append("Main proxy configured") status_parts.append(_("proxy.main_configured"))
if geo_proxy: if geo_proxy:
status_parts.append("Geo proxy configured") status_parts.append(_("proxy.geo_configured"))
self.proxy_status.setText(" | ".join(status_parts)) self.proxy_status.setText(" | ".join(status_parts))
self.proxy_status.setStyleSheet("color: #00cc00; font-style: italic;") self.proxy_status.setStyleSheet("color: #00cc00; font-style: italic;")
@@ -887,24 +892,24 @@ class CustomOptionsDialog(QDialog):
def run_custom_command(self) -> None: def run_custom_command(self) -> None:
url = self._parent.url_input.text().strip() url = self._parent.url_input.text().strip()
if not url: if not url:
self.log_output.append("❌ Error: No URL provided. Please enter a URL in the main window.") self.log_output.append(_("custom_command.error_no_url"))
return return
command = self.command_input.toPlainText().strip() command = self.command_input.toPlainText().strip()
if not command: if not command:
self.log_output.append("❌ Error: No command provided. Please enter yt-dlp arguments.") self.log_output.append(_("custom_command.error_no_command"))
return return
# Get download path from parent # Get download path from parent
path = self._parent.last_path path = self._parent.last_path
self.log_output.clear() self.log_output.clear()
self.log_output.append("🚀 Executing custom yt-dlp command") self.log_output.append(_("custom_command.executing"))
self.log_output.append(f"📍 URL: {url}") self.log_output.append(_("custom_command.url_label", url=url))
self.log_output.append(f"⚙️ Arguments: {command}") self.log_output.append(_("custom_command.args_label", command=command))
if path: if path:
self.log_output.append(f"📁 Download path: {path}") self.log_output.append(_("custom_command.download_path_label", path=path))
self.log_output.append("=" * 50) self.log_output.append(_("custom_command.separator"))
self.run_btn.setEnabled(False) self.run_btn.setEnabled(False)
self.run_btn.setText(_("command.running")) self.run_btn.setText(_("command.running"))
@@ -961,6 +966,12 @@ class CustomOptionsDialog(QDialog):
logger.info(f"Saving auto-update settings: enabled={enabled}, frequency={frequency}") logger.info(f"Saving auto-update settings: enabled={enabled}, frequency={frequency}")
result = update_auto_update_settings(enabled, frequency) result = update_auto_update_settings(enabled, frequency)
logger.info(f"Auto-update settings save result: {result}") logger.info(f"Auto-update settings save result: {result}")
# Save beta update setting
beta_enabled = self.updater_tab.get_beta_update_setting()
ConfigManager.set("check_beta_updates", beta_enabled)
logger.info(f"Saved beta updates setting: {beta_enabled}")
except Exception as e: except Exception as e:
logger.exception(f"Error saving auto-update settings: {e}") logger.exception(f"Error saving auto-update settings: {e}")
@@ -9,8 +9,9 @@ from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
from src.core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed from ...core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed
from src.utils.ytsage_constants import ICON_PATH from ...utils.ytsage_constants import ICON_PATH
from ...utils.ytsage_localization import _
class FFmpegInstallThread(QThread): class FFmpegInstallThread(QThread):
@@ -30,7 +31,7 @@ class FFmpegInstallThread(QThread):
class FFmpegCheckDialog(QDialog): class FFmpegCheckDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("FFmpeg Installation") self.setWindowTitle(_("ffmpeg.installation_title"))
self.setMinimumWidth(500) self.setMinimumWidth(500)
self.setMinimumHeight(280) self.setMinimumHeight(280)
self.resize(500, 300) self.resize(500, 300)
@@ -50,13 +51,13 @@ class FFmpegCheckDialog(QDialog):
layout.setContentsMargins(20, 20, 20, 20) layout.setContentsMargins(20, 20, 20, 20)
# Header with title and improved spacing # Header with title and improved spacing
header_text = QLabel("FFmpeg Installation") header_text = QLabel(_("ffmpeg.installation_title"))
header_text.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;") header_text.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;")
header_text.setAlignment(Qt.AlignmentFlag.AlignCenter) header_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(header_text) layout.addWidget(header_text)
# Message # Message
self.message_label = QLabel("YTSage needs FFmpeg to process videos.\n\n" "Choose an installation option below:") self.message_label = QLabel(_("ffmpeg.installation_message"))
self.message_label.setWordWrap(True) self.message_label.setWordWrap(True)
self.message_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 10px 0; line-height: 1.4;") self.message_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 10px 0; line-height: 1.4;")
self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -93,17 +94,17 @@ class FFmpegCheckDialog(QDialog):
button_layout.setSpacing(12) button_layout.setSpacing(12)
# Install button # Install button
self.install_btn = QPushButton("Install FFmpeg") self.install_btn = QPushButton(_("ffmpeg.install_button"))
self.install_btn.clicked.connect(self.start_installation) self.install_btn.clicked.connect(self.start_installation)
button_layout.addWidget(self.install_btn) button_layout.addWidget(self.install_btn)
# Manual install button # Manual install button
self.manual_btn = QPushButton("Manual Guide") self.manual_btn = QPushButton(_("ffmpeg.manual_guide"))
self.manual_btn.clicked.connect(lambda: webbrowser.open("https://github.com/oop7/ffmpeg-install-guide")) self.manual_btn.clicked.connect(lambda: webbrowser.open("https://github.com/oop7/ffmpeg-install-guide"))
button_layout.addWidget(self.manual_btn) button_layout.addWidget(self.manual_btn)
# Close button # Close button
self.close_btn = QPushButton("Close") self.close_btn = QPushButton(_("buttons.close"))
self.close_btn.clicked.connect(self.close) self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.close_btn) button_layout.addWidget(self.close_btn)
@@ -153,15 +154,15 @@ class FFmpegCheckDialog(QDialog):
# Check if FFmpeg is already installed # Check if FFmpeg is already installed
if check_ffmpeg_installed(): if check_ffmpeg_installed():
self.message_label.setText("FFmpeg is already installed!") self.message_label.setText(_("ffmpeg.already_installed"))
self.progress_label.setText("Installation complete. You can close this dialog and continue using YTSage.") self.progress_label.setText(_("ffmpeg.installation_complete"))
self.progress_label.show() self.progress_label.show()
self.install_btn.hide() self.install_btn.hide()
self.manual_btn.hide() self.manual_btn.hide()
self.close_btn.setEnabled(True) self.close_btn.setEnabled(True)
return return
self.message_label.setText("Installing FFmpeg... Please wait") self.message_label.setText(_("ffmpeg.installing"))
self.progress_messages = [] # Clear previous messages self.progress_messages = [] # Clear previous messages
self.progress_label.show() self.progress_label.show()
@@ -181,13 +182,13 @@ class FFmpegCheckDialog(QDialog):
def installation_finished(self, success) -> None: def installation_finished(self, success) -> None:
if success: if success:
self.message_label.setText("FFmpeg has been installed successfully!") self.message_label.setText(_("ffmpeg.install_success"))
self.progress_label.setText("Installation complete. You can now close this dialog and continue using YTSage.") self.progress_label.setText(_("ffmpeg.installation_complete_close"))
self.install_btn.hide() self.install_btn.hide()
self.manual_btn.hide() self.manual_btn.hide()
else: else:
self.message_label.setText("FFmpeg installation encountered an issue.") self.message_label.setText(_("ffmpeg.installation_failed"))
self.progress_label.setText("Please try using the manual installation guide instead.") self.progress_label.setText(_("ffmpeg.try_manual"))
self.install_btn.setEnabled(True) self.install_btn.setEnabled(True)
self.manual_btn.setEnabled(True) self.manual_btn.setEnabled(True)
@@ -0,0 +1,576 @@
"""
History Dialog for YTSage application.
Displays download history with thumbnails using a virtualized list for performance.
"""
import os
import subprocess
import json
from datetime import datetime
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Optional, List, Any, Dict
import requests
from PIL import Image
from PySide6.QtCore import (
Qt, QSize, Signal, QThread, QTimer, QAbstractListModel,
QModelIndex, QRect, QPoint, QEvent
)
from PySide6.QtGui import (
QPixmap, QIcon, QPainter, QColor, QFont, QBrush, QPen,
QMouseEvent, QDesktopServices, QAction, QCursor, QPainterPath
)
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QListView,
QWidget,
QMenu,
QMessageBox,
QStyledItemDelegate,
QStyle,
QApplication
)
from ...utils.ytsage_history_manager import HistoryManager
from ...utils.ytsage_constants import APP_THUMBNAILS_DIR, SUBPROCESS_CREATIONFLAGS
from ...utils.ytsage_localization import _
from ...utils.ytsage_logger import logger
if TYPE_CHECKING:
from ..ytsage_gui_main import YTSageApp
class HistoryLoaderThread(QThread):
"""Thread to load history and pre-fetch thumbnails."""
entries_loaded = Signal(list)
thumbnail_loaded = Signal(str, bytes) # entry_id, image_bytes
def run(self):
try:
# Load entries from DB
entries = HistoryManager.get_all_entries()
self.entries_loaded.emit(entries)
# Background thumbnail loader
for entry in entries:
if self.isInterruptionRequested():
break
thumbnail_url = entry.get("thumbnail_url")
entry_id = entry.get("id", "")
if not thumbnail_url or not entry_id:
continue
thumbnail_filename = f"{entry_id}.jpg"
thumbnail_path = APP_THUMBNAILS_DIR / thumbnail_filename
if not thumbnail_path.exists():
try:
response = requests.get(thumbnail_url, timeout=5)
if response.status_code == 200:
APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
# Optimize image before saving
img_io = BytesIO(response.content)
image = Image.open(img_io)
# Save to disk
image.save(thumbnail_path, "JPEG", quality=90, optimize=True)
# Emit bytes for memory cache
self.thumbnail_loaded.emit(entry_id, response.content)
except Exception as e:
logger.debug(f"Error caching thumbnail: {e}")
except Exception as e:
logger.error(f"Error loading history: {e}")
self.entries_loaded.emit([])
class HistoryModel(QAbstractListModel):
"""List Model for History Entries."""
EntryRole = Qt.ItemDataRole.UserRole + 1
IdRole = Qt.ItemDataRole.UserRole + 2
ThumbnailRole = Qt.ItemDataRole.UserRole + 3
def __init__(self, entries=None, parent=None):
super().__init__(parent)
self._entries = entries or []
self.thumbnail_cache = {} # Map entry_id -> QPixmap
def rowCount(self, parent=QModelIndex()):
return len(self._entries)
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if not index.isValid() or not (0 <= index.row() < len(self._entries)):
return None
entry = self._entries[index.row()]
entry_id = entry.get("id")
if role == self.EntryRole:
return entry
elif role == self.IdRole:
return entry_id
elif role == self.ThumbnailRole:
return self.thumbnail_cache.get(entry_id)
elif role == Qt.ItemDataRole.DisplayRole:
return entry.get("title", "")
return None
def update_entries(self, entries):
self.beginResetModel()
self._entries = entries
self.endResetModel()
def remove_item(self, row):
if 0 <= row < len(self._entries):
self.beginRemoveRows(QModelIndex(), row, row)
del self._entries[row]
self.endRemoveRows()
def update_thumbnail(self, entry_id, pixmap):
"""Update cache and notify view."""
self.thumbnail_cache[entry_id] = pixmap
# Find index for this ID
for i, entry in enumerate(self._entries):
if entry.get("id") == entry_id:
idx = self.index(i)
self.dataChanged.emit(idx, idx, [self.ThumbnailRole])
break
class HistoryDelegate(QStyledItemDelegate):
"""Delegate to render history cards similar to the widgets."""
menu_clicked = Signal(QModelIndex, QPoint) # Signal for menu click
def __init__(self, parent=None):
super().__init__(parent)
self.padding = 10
self.thumb_width = 240
self.thumb_height = 135
# Increased card height to accommodate spacing
self.card_height = 175
# Define margins for spacing between cards
self.h_margin = 10
self.v_margin = 8
def sizeHint(self, option, index):
return QSize(option.rect.width(), self.card_height)
def paint(self, painter, option, index):
entry = index.data(HistoryModel.EntryRole)
if not entry:
return
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
rect = option.rect
# Apply margins for spacing
card_rect = rect.adjusted(self.h_margin, self.v_margin, -self.h_margin, -self.v_margin)
is_hover = option.state & QStyle.StateFlag.State_MouseOver
bg_color = QColor("#252830") if is_hover else QColor("#1d1e22")
border_color = QColor("#3a3d46") if is_hover else QColor("#2a2d36")
# Draw Card
path = QPainterPath()
path.addRoundedRect(card_rect, 8, 8)
painter.fillPath(path, QBrush(bg_color))
painter.setPen(QPen(border_color, 1))
painter.drawPath(path)
# Draw Thumbnail
thumb_rect = QRect(
card_rect.left() + 10,
card_rect.top() + 10,
self.thumb_width,
self.thumb_height
)
pixmap = index.data(HistoryModel.ThumbnailRole)
if pixmap and not pixmap.isNull():
scaled = pixmap.scaled(
thumb_rect.size(),
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation
)
# Clip to rect
painter.setClipRect(thumb_rect)
painter.drawPixmap(thumb_rect.topLeft(), scaled)
painter.setClipping(False)
else:
painter.fillRect(thumb_rect, QColor("#15181b"))
painter.setPen(QPen(QColor("#666666")))
icon_char = "🎵" if entry.get("is_audio_only") else "📹"
painter.setFont(QFont("Segoe UI Emoji", 24))
painter.drawText(thumb_rect, Qt.AlignmentFlag.AlignCenter, icon_char)
# Draw Border around thumb
painter.setPen(QPen(QColor("#3d3d3d"), 2))
painter.drawRect(thumb_rect)
# Text Area
text_x = thumb_rect.right() + 12
text_width = card_rect.right() - text_x - 85 # Leave even more space for the larger 60px button
# Title
title_rect = QRect(text_x, thumb_rect.top(), text_width, 50)
painter.setPen(QColor("#ffffff"))
font_title = QFont()
font_title.setBold(True)
font_title.setPixelSize(14)
painter.setFont(font_title)
# Use simple alignment flags
painter.drawText(title_rect, Qt.AlignmentFlag.AlignLeft | Qt.TextFlag.TextWordWrap, entry.get("title", ""))
current_y = title_rect.bottom() + 5
# Channel
channel = entry.get("channel")
if channel:
painter.setPen(QColor("#cccccc"))
font_meta = QFont()
font_meta.setPixelSize(12)
painter.setFont(font_meta)
painter.drawText(text_x, current_y, f"{_('video_info.channel')}: {channel}")
current_y += 18
# Date
date_str = entry.get("download_date", "")[:16].replace('T', ' ')
if date_str:
painter.setPen(QColor("#aaaaaa"))
painter.setFont(QFont("Arial", 11))
painter.drawText(text_x, current_y, f"{_('history.downloaded_on', date=date_str)}")
current_y += 25
# Badge
is_audio = entry.get("is_audio_only", False)
badge_text = _("history.audio_download") if is_audio else _("history.video_download")
badge_color = QColor("#0066cc") if is_audio else QColor("#c90000")
badge_rect = QRect(text_x, current_y, 80, 20)
painter.setBrush(QBrush(badge_color))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(badge_rect, 3, 3)
painter.setPen(QColor("white"))
font_badge = QFont()
font_badge.setBold(True)
font_badge.setPixelSize(10)
painter.setFont(font_badge)
painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, badge_text)
# File Size
file_size = entry.get("file_size", 0)
if file_size > 0:
size_str = self.format_file_size(file_size)
painter.setPen(QColor("#aaaaaa"))
painter.setFont(QFont("Arial", 11))
painter.drawText(badge_rect.right() + 10, current_y + 14, size_str)
# Menu Button
menu_rect = self.get_menu_rect(card_rect)
# Check hover on menu button specifically
mouse_pos = QCursor.pos()
if option.widget:
mouse_pos = option.widget.mapFromGlobal(mouse_pos)
if menu_rect.contains(mouse_pos):
painter.setPen(QColor("#c90000"))
else:
painter.setPen(QColor("#ffffff"))
painter.setFont(QFont("Arial", 18, QFont.Weight.Bold))
painter.drawText(menu_rect, Qt.AlignmentFlag.AlignCenter, "")
painter.restore()
def get_menu_rect(self, card_rect):
# Widen the clickable area significantly (60x50) and shift slightly left
# to fix "left side not working" issues.
w = 60
h = 50
margin_right = 5
margin_top = 5
return QRect(
card_rect.right() - w - margin_right,
card_rect.top() + margin_top,
w,
h
)
def editorEvent(self, event, model, option, index):
"""Handle mouse clicks."""
if event.type() == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton:
# Use same margins as paint to ensure hit consistency
card_rect = option.rect.adjusted(self.h_margin, self.v_margin, -self.h_margin, -self.v_margin)
menu_rect = self.get_menu_rect(card_rect)
if menu_rect.contains(event.pos()):
self.menu_clicked.emit(index, event.globalPos())
return True
return super().editorEvent(event, model, option, index)
def format_file_size(self, size_bytes: int) -> str:
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
class HistoryDialog(QDialog):
"""Dialog to display and manage download history."""
redownload_requested = Signal(dict)
def __init__(self, parent: Optional["YTSageApp"] = None):
super().__init__(parent)
self.parent_app = parent
self.setup_ui()
self.show_loading_state()
QTimer.singleShot(100, self.start_loading_history)
def setup_ui(self):
self.setWindowTitle(_("history.title"))
self.resize(850, 600)
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint)
self.setStyleSheet("""
QDialog { background-color: #15181b; }
QLabel { color: #ffffff; }
""")
layout = QVBoxLayout(self)
# --- Header ---
header = QHBoxLayout()
title = QLabel(_("history.title"))
title.setStyleSheet("font-size: 18px; font-weight: bold;")
header.addWidget(title)
header.addStretch()
self.clear_btn = QPushButton(_("history.clear_all"))
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #c90000; color: white; padding: 6px 12px;
border: none; border-radius: 4px; font-weight: bold;
}
QPushButton:hover { background-color: #a50000; }
QPushButton:disabled { background-color: #555555; color: #aaaaaa; }
""")
self.clear_btn.clicked.connect(self.clear_all_history)
header.addWidget(self.clear_btn)
layout.addLayout(header)
# --- Search ---
self.search_input = QLineEdit()
self.search_input.setPlaceholderText(_("history.search_placeholder"))
self.search_input.setStyleSheet("""
QLineEdit {
padding: 8px; border: 2px solid #2a2d36; border-radius: 4px;
background-color: #1b2021; color: white;
}
""")
self.search_input.textChanged.connect(self.filter_history)
layout.addWidget(self.search_input)
# --- List View ---
self.list_view = QListView()
self.list_view.setStyleSheet("""
QListView {
background-color: transparent;
border: none;
outline: none;
}
QListView::item {
border: none;
background: transparent;
}
""")
self.list_view.setVerticalScrollMode(QListView.ScrollMode.ScrollPerPixel)
self.list_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.list_view.setUniformItemSizes(True)
self.list_view.setSelectionMode(QListView.SelectionMode.NoSelection)
self.list_view.setMouseTracking(True)
self.list_view.setResizeMode(QListView.ResizeMode.Adjust)
self.model = HistoryModel([], self)
self.list_view.setModel(self.model)
self.delegate = HistoryDelegate(self.list_view)
self.delegate.menu_clicked.connect(self.show_context_menu)
self.list_view.setItemDelegate(self.delegate)
self.list_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.list_view.customContextMenuRequested.connect(self.on_context_menu_requested)
layout.addWidget(self.list_view)
# --- Status ---
self.status_label = QLabel()
self.status_label.setStyleSheet("color: #aaaaaa; font-size: 12px;")
layout.addWidget(self.status_label)
def show_loading_state(self):
self.status_label.setText(_("history.loading"))
self.clear_btn.setEnabled(False)
def start_loading_history(self):
self.loader_thread = HistoryLoaderThread()
self.loader_thread.entries_loaded.connect(self.on_entries_loaded)
self.loader_thread.thumbnail_loaded.connect(self.on_thumbnail_loaded)
self.loader_thread.start()
def on_entries_loaded(self, entries):
self.model.update_entries(entries)
count = len(entries)
if count == 0:
self.status_label.setText(_("history.no_history"))
self.clear_btn.setEnabled(False)
else:
self.status_label.setText(_("history.entries_count", count=count))
self.clear_btn.setEnabled(True)
self.load_cached_thumbnails(entries)
def load_cached_thumbnails(self, entries):
for entry in entries:
eid = entry.get("id")
if not eid: continue
p = APP_THUMBNAILS_DIR / f"{eid}.jpg"
if p.exists():
pix = QPixmap(str(p))
if not pix.isNull():
self.model.thumbnail_cache[eid] = pix
def on_thumbnail_loaded(self, entry_id, data_bytes):
pixmap = QPixmap()
pixmap.loadFromData(data_bytes)
if not pixmap.isNull():
self.model.update_thumbnail(entry_id, pixmap)
def on_context_menu_requested(self, pos):
index = self.list_view.indexAt(pos)
if index.isValid():
global_pos = self.list_view.mapToGlobal(pos)
self.show_context_menu(index, global_pos)
def show_context_menu(self, index, global_pos):
entry = index.data(HistoryModel.EntryRole)
if not entry: return
menu = QMenu(self)
menu.setStyleSheet("""
QMenu {
background-color: #2a2d36; border: 1px solid #3a3d46; color: white;
}
QMenu::item {
padding: 8px 20px;
}
QMenu::item:selected {
background-color: #c90000;
}
""")
act_open = menu.addAction("📁 " + _("history.open_location"))
act_redownload = menu.addAction("⬇️ " + _("history.redownload"))
menu.addSeparator()
act_remove = menu.addAction("🗑️ " + _("history.remove"))
action = menu.exec(global_pos)
if action == act_open:
self.open_file_location(entry)
elif action == act_redownload:
self.redownload_entry(entry)
elif action == act_remove:
self.remove_entry(index)
def open_file_location(self, entry):
path_str = entry.get("file_path", "")
if not path_str: return
path = Path(path_str)
if not path.exists():
QMessageBox.warning(self, _("main_ui.error_title"), _("history.file_not_found_message", path=path))
return
try:
if os.name == "nt":
subprocess.run(['explorer', '/select,', str(path)], creationflags=SUBPROCESS_CREATIONFLAGS)
elif subprocess.sys.platform == "darwin":
subprocess.run(['open', '-R', str(path)])
else:
folder_path = path.parent
subprocess.run(['xdg-open', str(folder_path)])
except Exception as e:
logger.error(f"Failed to open file: {e}")
def redownload_entry(self, entry):
reply = QMessageBox.question(
self,
_("history.redownload_confirm_title"),
_("history.redownload_confirm_message", title=entry.get("title")),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.redownload_requested.emit(entry)
self.accept()
def remove_entry(self, index):
entry = index.data(HistoryModel.EntryRole)
reply = QMessageBox.question(
self,
_("history.remove_confirm_title"),
_("history.remove_confirm_message", title=entry.get("title")),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
if HistoryManager.remove_entry(entry.get("id")):
self.model.remove_item(index.row())
self.status_label.setText(
_("history.entries_count", count=self.model.rowCount())
)
def clear_all_history(self):
reply = QMessageBox.question(
self,
_("history.clear_confirm_title"),
_("history.clear_confirm_message"),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
HistoryManager.clear_history()
self.model.update_entries([])
self.clear_btn.setEnabled(False)
self.status_label.setText(_("history.no_history"))
def filter_history(self, query):
results = HistoryManager.search_entries(query)
self.model.update_entries(results)
self.load_cached_thumbnails(results)
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.utils.ytsage_localization import _ from ...utils.ytsage_localization import _
class SubtitleSelectionDialog(QDialog): class SubtitleSelectionDialog(QDialog):
@@ -208,6 +208,27 @@ class PlaylistSelectionDialog(QDialog):
# Main layout # Main layout
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
# Filter Input
self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText(_("dialogs.filter_playlist_placeholder"))
self.filter_input.textChanged.connect(self.filter_list)
self.filter_input.setStyleSheet(
"""
QLineEdit {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px;
min-height: 30px;
color: white;
}
QLineEdit:focus {
border-color: #ff0000;
}
"""
)
main_layout.addWidget(self.filter_input)
# Top buttons (Select/Deselect All) # Top buttons (Select/Deselect All)
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
select_all_btn = QPushButton(_("buttons.select_all")) select_all_btn = QPushButton(_("buttons.select_all"))
@@ -339,6 +360,13 @@ class PlaylistSelectionDialog(QDialog):
pass # Ignore invalid numbers pass # Ignore invalid numbers
return selected_indices return selected_indices
def filter_list(self, text: str) -> None:
"""Filter the list of checkboxes based on title."""
text = text.lower()
for checkbox in self.checkboxes:
title = (checkbox.property("full_title") or "").lower()
checkbox.setVisible(text in title)
def _populate_list(self, previously_selected_string) -> None: def _populate_list(self, previously_selected_string) -> None:
"""Populates the scroll area with checkboxes for each video.""" """Populates the scroll area with checkboxes for each video."""
selected_indices = self._parse_selection_string(previously_selected_string) selected_indices = self._parse_selection_string(previously_selected_string)
@@ -356,12 +384,29 @@ class PlaylistSelectionDialog(QDialog):
video_index = index + 1 # yt-dlp uses 1-based indexing video_index = index + 1 # yt-dlp uses 1-based indexing
title = entry.get("title", f"Video {video_index}") title = entry.get("title", f"Video {video_index}")
# Shorten title if too long
display_title = (title[:70] + "...") if len(title) > 73 else title
checkbox = QCheckBox(f"{video_index}. {display_title}") # Format duration
duration = entry.get("duration")
duration_str = ""
if duration:
try:
m, s = divmod(int(duration), 60)
h, m = divmod(m, 60)
if h > 0:
duration_str = f" [{h}:{m:02d}:{s:02d}]"
else:
duration_str = f" [{m:02d}:{s:02d}]"
except (ValueError, TypeError):
pass
# Shorten title if too long but keep enough space for duration
max_len = 65
display_title = (title[:max_len] + "...") if len(title) > max_len + 3 else title
checkbox = QCheckBox(f"{video_index}. {display_title}{duration_str}")
checkbox.setChecked(video_index in selected_indices) checkbox.setChecked(video_index in selected_indices)
checkbox.setProperty("video_index", video_index) # Store index checkbox.setProperty("video_index", video_index) # Store index
checkbox.setProperty("full_title", title) # Store full title for filtering
checkbox.setStyleSheet( checkbox.setStyleSheet(
""" """
QCheckBox { QCheckBox {
@@ -9,7 +9,8 @@ from datetime import datetime
import requests import requests
from packaging import version as version_parser from packaging import version as version_parser
from PySide6.QtCore import Qt, QTimer from PySide6.QtCore import Qt, QTimer, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QButtonGroup, QButtonGroup,
QCheckBox, QCheckBox,
@@ -27,9 +28,10 @@ from PySide6.QtWidgets import (
QVBoxLayout, QVBoxLayout,
) )
from src.utils.ytsage_logger import logger from ...utils.ytsage_logger import logger
from src.utils.ytsage_localization import _ from ...utils.ytsage_localization import _
from src.utils.ytsage_config_manager import ConfigManager from ...utils.ytsage_config_manager import ConfigManager
from ...utils.ytsage_constants import APP_LOG_DIR
class DownloadSettingsDialog(QDialog): class DownloadSettingsDialog(QDialog):
@@ -228,7 +230,7 @@ class DownloadSettingsDialog(QDialog):
# Help text # Help text
help_label = QLabel(_("settings.force_format_help")) help_label = QLabel(_("settings.force_format_help"))
help_label.setWordWrap(True) help_label.setWordWrap(True)
help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
output_format_layout.addWidget(help_label) output_format_layout.addWidget(help_label)
output_format_group_box.setLayout(output_format_layout) output_format_group_box.setLayout(output_format_layout)
@@ -274,12 +276,41 @@ class DownloadSettingsDialog(QDialog):
# Help text for audio format # Help text for audio format
audio_help_label = QLabel(_("settings.force_audio_format_help")) audio_help_label = QLabel(_("settings.force_audio_format_help"))
audio_help_label.setWordWrap(True) audio_help_label.setWordWrap(True)
audio_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") audio_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
audio_format_layout.addWidget(audio_help_label) audio_format_layout.addWidget(audio_help_label)
audio_format_group_box.setLayout(audio_format_layout) audio_format_group_box.setLayout(audio_format_layout)
layout.addWidget(audio_format_group_box) layout.addWidget(audio_format_group_box)
# --- Filename Format Section ---
filename_format_group_box = QGroupBox(_("settings.filename_format"))
filename_layout = QVBoxLayout()
# Load current filename format from ConfigManager
self.filename_format_value = ConfigManager.get("filename_format") or "%(title)s_%(resolution)s.%(ext)s"
# Input and Reset Button Layout
filename_input_layout = QHBoxLayout()
self.filename_format_input = QLineEdit(self.filename_format_value)
self.filename_format_input.setPlaceholderText("%(title)s_%(resolution)s.%(ext)s")
filename_input_layout.addWidget(self.filename_format_input)
self.reset_format_button = QPushButton(_("buttons.reset"))
self.reset_format_button.setFixedWidth(70)
self.reset_format_button.clicked.connect(lambda: self.filename_format_input.setText("%(title)s_%(resolution)s.%(ext)s"))
filename_input_layout.addWidget(self.reset_format_button)
filename_layout.addLayout(filename_input_layout)
filename_help_label = QLabel(_("settings.filename_format_help"))
filename_help_label.setWordWrap(True)
filename_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
filename_layout.addWidget(filename_help_label)
filename_format_group_box.setLayout(filename_layout)
layout.addWidget(filename_format_group_box)
# Dialog buttons (OK/Cancel) # Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox() button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole) ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
@@ -332,6 +363,10 @@ class DownloadSettingsDialog(QDialog):
audio_format_map = {0: "best", 1: "aac", 2: "mp3", 3: "flac", 4: "wav", 5: "opus", 6: "m4a", 7: "vorbis"} audio_format_map = {0: "best", 1: "aac", 2: "mp3", 3: "flac", 4: "wav", 5: "opus", 6: "m4a", 7: "vorbis"}
return audio_format_map.get(self.audio_format_combo.currentIndex(), "best") return audio_format_map.get(self.audio_format_combo.currentIndex(), "best")
def get_filename_format(self) -> str:
"""Returns the filename format string."""
return self.filename_format_input.text().strip()
def _create_styled_message_box(self, icon, title, text) -> QMessageBox: def _create_styled_message_box(self, icon, title, text) -> QMessageBox:
"""Create a styled QMessageBox that matches the app theme.""" """Create a styled QMessageBox that matches the app theme."""
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
@@ -382,6 +417,11 @@ class DownloadSettingsDialog(QDialog):
ConfigManager.set("force_audio_format", force_audio_format) ConfigManager.set("force_audio_format", force_audio_format)
ConfigManager.set("preferred_audio_format", preferred_audio_format) ConfigManager.set("preferred_audio_format", preferred_audio_format)
# Save filename format
filename_format = self.get_filename_format()
if filename_format:
ConfigManager.set("filename_format", filename_format)
QMessageBox.information( QMessageBox.information(
self, self,
_("settings.settings_saved_title"), _("settings.settings_saved_title"),
@@ -573,7 +613,7 @@ class AutoUpdateSettingsDialog(QDialog):
# Update status labels # Update status labels
current_version = get_ytdlp_version() current_version = get_ytdlp_version()
self.current_version_label.setText(f"Current yt-dlp version: {current_version}") self.current_version_label.setText(_("auto_update.current_version", version=current_version))
last_check = settings["last_check"] last_check = settings["last_check"]
if last_check > 0: if last_check > 0:
@@ -14,15 +14,16 @@ from packaging import version
from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtCore import Qt, QThread, QTimer, Signal
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config from ...core.ytsage_utils import get_ytdlp_version
from src.core.ytsage_yt_dlp import get_yt_dlp_path from ...core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL from ...utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
from src.utils.ytsage_localization import LocalizationManager from ...utils.ytsage_config_manager import ConfigManager
from ...utils.ytsage_localization import LocalizationManager
# Shorthand for localization # Shorthand for localization
_ = LocalizationManager.get_text _ = LocalizationManager.get_text
from src.utils.ytsage_localization import _ from ...utils.ytsage_localization import _
from src.utils.ytsage_logger import logger from ...utils.ytsage_logger import logger
class VersionCheckThread(QThread): class VersionCheckThread(QThread):
@@ -407,9 +408,7 @@ class AutoUpdateThread(QThread):
if success: if success:
logger.info("AutoUpdateThread: Auto-update completed successfully!") logger.info("AutoUpdateThread: Auto-update completed successfully!")
# Update the last check timestamp # Update the last check timestamp
config = load_config() ConfigManager.set("last_update_check", time.time())
config["last_update_check"] = time.time()
save_config(config)
self.update_finished.emit( self.update_finished.emit(
True, True,
f"Successfully updated yt-dlp from {current_version} to {latest_version}", f"Successfully updated yt-dlp from {current_version} to {latest_version}",
@@ -420,9 +419,7 @@ class AutoUpdateThread(QThread):
else: else:
logger.info("AutoUpdateThread: yt-dlp is already up to date") logger.info("AutoUpdateThread: yt-dlp is already up to date")
# Still update the timestamp even if no update was needed # Still update the timestamp even if no update was needed
config = load_config() ConfigManager.set("last_update_check", time.time())
config["last_update_check"] = time.time()
save_config(config)
self.update_finished.emit( self.update_finished.emit(
True, True,
f"yt-dlp is already up to date (version {current_version})", f"yt-dlp is already up to date (version {current_version})",
@@ -9,7 +9,7 @@ import threading
from typing import Optional, Tuple, TYPE_CHECKING, cast from typing import Optional, Tuple, TYPE_CHECKING, cast
import requests import requests
from PySide6.QtCore import Qt, Signal from PySide6.QtCore import Qt, Signal, QThread, Slot
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QCheckBox, QCheckBox,
QGroupBox, QGroupBox,
@@ -23,25 +23,25 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.core.ytsage_utils import ( from ...core.ytsage_utils import (
get_auto_update_settings, get_auto_update_settings,
get_ffmpeg_version_direct, get_ffmpeg_version_direct,
update_auto_update_settings, update_auto_update_settings,
) )
from src.core.ytsage_yt_dlp import get_yt_dlp_path from ...core.ytsage_yt_dlp import get_yt_dlp_path
from src.core.ytsage_deno import check_deno_update, upgrade_deno from ...core.ytsage_deno import check_deno_update, upgrade_deno
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import YTDLPUpdateDialog from .ytsage_dialogs_update import YTDLPUpdateDialog
from src.utils.ytsage_config_manager import ConfigManager from ...utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_localization import _ from ...utils.ytsage_localization import _
from src.utils.ytsage_logger import logger from ...utils.ytsage_logger import logger
from src.utils.ytsage_constants import ( from ...utils.ytsage_constants import (
FFMPEG_7Z_VERSION_URL, FFMPEG_7Z_VERSION_URL,
OS_NAME, OS_NAME,
SUBPROCESS_CREATIONFLAGS, SUBPROCESS_CREATIONFLAGS,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog from .ytsage_dialogs_custom import CustomOptionsDialog
# Helper functions for FFmpeg version checking (copied from removed ytsage_ffmpeg_updater.py) # Helper functions for FFmpeg version checking (copied from removed ytsage_ffmpeg_updater.py)
@@ -157,6 +157,47 @@ def check_ffmpeg_version() -> Tuple[bool, str, str]:
return False, "Error", "Error" return False, "Error", "Error"
class FFmpegCheckThread(QThread):
finished = Signal(bool, str, str)
error = Signal(str)
def run(self):
try:
update_available, current_version, latest_version = check_ffmpeg_version()
self.finished.emit(update_available, current_version, latest_version)
except Exception as e:
logger.exception(f"Error checking FFmpeg version: {e}")
self.error.emit(str(e))
class DenoCheckThread(QThread):
finished = Signal(bool, str, str)
error = Signal(str)
def run(self):
try:
update_available, current_version, latest_version = check_deno_update()
self.finished.emit(update_available, current_version, latest_version)
except Exception as e:
logger.exception(f"Error checking Deno version: {e}")
self.error.emit(str(e))
class DenoUpdateThread(QThread):
finished = Signal(bool, str)
progress = Signal(str)
error = Signal(str)
def run(self):
try:
success, output = upgrade_deno(progress_callback=self.progress.emit)
self.finished.emit(success, output)
except Exception as e:
logger.exception(f"Error updating Deno: {e}")
self.error.emit(str(e))
class UpdaterTabWidget(QWidget): class UpdaterTabWidget(QWidget):
"""Widget for the Updater tab in Custom Options dialog.""" """Widget for the Updater tab in Custom Options dialog."""
@@ -401,6 +442,40 @@ class UpdaterTabWidget(QWidget):
layout.addWidget(deno_group) layout.addWidget(deno_group)
# === App Updates Section ===
app_update_group = QGroupBox(_("settings.app_updates_title"))
app_update_layout = QVBoxLayout()
self.beta_updates_checkbox = QCheckBox(_("settings.check_beta_updates"))
self.beta_updates_checkbox.setStyleSheet(
"""
QCheckBox {
color: #ffffff;
spacing: 5px;
padding: 3px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
"""
)
app_update_layout.addWidget(self.beta_updates_checkbox)
app_update_group.setLayout(app_update_layout)
layout.addWidget(app_update_group)
# === yt-dlp Release Channel Section === # === yt-dlp Release Channel Section ===
ytdlp_channel_group = QGroupBox(_("settings.ytdlp_channel")) ytdlp_channel_group = QGroupBox(_("settings.ytdlp_channel"))
ytdlp_channel_layout = QVBoxLayout() ytdlp_channel_layout = QVBoxLayout()
@@ -466,6 +541,32 @@ class UpdaterTabWidget(QWidget):
# Enable/Disable auto-update checkbox # Enable/Disable auto-update checkbox
self.auto_update_enabled = QCheckBox(_("settings.enable_auto_updates")) self.auto_update_enabled = QCheckBox(_("settings.enable_auto_updates"))
self.auto_update_enabled.setStyleSheet(
"""
QCheckBox {
color: #ffffff;
spacing: 5px;
padding: 3px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
QCheckBox:disabled { color: #888888; }
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
"""
)
auto_update_layout.addWidget(self.auto_update_enabled) auto_update_layout.addWidget(self.auto_update_enabled)
# Frequency options # Frequency options
@@ -551,6 +652,10 @@ class UpdaterTabWidget(QWidget):
# Set checkbox # Set checkbox
self.auto_update_enabled.setChecked(auto_settings["enabled"]) self.auto_update_enabled.setChecked(auto_settings["enabled"])
# Load beta setting
beta_enabled = ConfigManager.get("check_beta_updates") or False
self.beta_updates_checkbox.setChecked(beta_enabled)
# Set current selection based on saved settings # Set current selection based on saved settings
current_frequency = auto_settings["frequency"] current_frequency = auto_settings["frequency"]
if current_frequency == "startup": if current_frequency == "startup":
@@ -589,6 +694,10 @@ class UpdaterTabWidget(QWidget):
return enabled, frequency return enabled, frequency
def get_beta_update_setting(self) -> bool:
"""Returns the beta update setting from the dialog."""
return self.beta_updates_checkbox.isChecked()
def _on_channel_changed(self, checked: bool) -> None: def _on_channel_changed(self, checked: bool) -> None:
"""Handle channel selection change.""" """Handle channel selection change."""
if not checked: if not checked:
@@ -637,6 +746,11 @@ class UpdaterTabWidget(QWidget):
response.raise_for_status() response.raise_for_status()
latest_tag = response.json()["info"]["version"] latest_tag = response.json()["info"]["version"]
if latest_tag: if latest_tag:
# PyPI returns version without zero-padding (e.g. "2026.2.21")
# but yt-dlp GitHub tags are zero-padded (e.g. "2026.02.21")
parts = latest_tag.split(".")
if len(parts) == 3:
latest_tag = f"{parts[0]}.{int(parts[1]):02d}.{int(parts[2]):02d}"
update_target = f"stable@{latest_tag}" update_target = f"stable@{latest_tag}"
logger.info(f"Latest stable version tag: {latest_tag}") logger.info(f"Latest stable version tag: {latest_tag}")
else: else:
@@ -754,22 +868,20 @@ class UpdaterTabWidget(QWidget):
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
) )
# Run check in background thread self.ffmpeg_check_thread = FFmpegCheckThread()
def check_thread(): self.ffmpeg_check_thread.finished.connect(self._on_ffmpeg_check_finished)
try: self.ffmpeg_check_thread.error.connect(self._on_ffmpeg_check_error)
update_available, current_version, latest_version = check_ffmpeg_version() self.ffmpeg_check_thread.start()
# Update UI in main thread @Slot(bool, str, str)
def _on_ffmpeg_check_finished(self, update_available, current_version, latest_version):
self.check_button.setEnabled(True) self.check_button.setEnabled(True)
self._update_check_results(update_available, current_version, latest_version) self._update_check_results(update_available, current_version, latest_version)
except Exception as e: @Slot(str)
logger.exception(f"Error checking FFmpeg version: {e}") def _on_ffmpeg_check_error(self, error):
self.check_button.setEnabled(True) self.check_button.setEnabled(True)
self._show_check_error(str(e)) self._show_check_error(error)
thread = threading.Thread(target=check_thread, daemon=True)
thread.start()
def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None: def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None:
"""Handle completion of version check.""" """Handle completion of version check."""
@@ -819,22 +931,20 @@ class UpdaterTabWidget(QWidget):
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
) )
# Run check in background thread self.deno_check_thread = DenoCheckThread()
def check_thread(): self.deno_check_thread.finished.connect(self._on_deno_check_finished)
try: self.deno_check_thread.error.connect(self._on_deno_check_error)
update_available, current_version, latest_version = check_deno_update() self.deno_check_thread.start()
# Update UI in main thread @Slot(bool, str, str)
def _on_deno_check_finished(self, update_available, current_version, latest_version):
self.deno_check_button.setEnabled(True) self.deno_check_button.setEnabled(True)
self._update_deno_check_results(update_available, current_version, latest_version) self._update_deno_check_results(update_available, current_version, latest_version)
except Exception as e: @Slot(str)
logger.exception(f"Error checking Deno version: {e}") def _on_deno_check_error(self, error):
self.deno_check_button.setEnabled(True) self.deno_check_button.setEnabled(True)
self._show_deno_check_error(str(e)) self._show_deno_check_error(error)
thread = threading.Thread(target=check_thread, daemon=True)
thread.start()
def _update_deno_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None: def _update_deno_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None:
"""Handle completion of Deno version check.""" """Handle completion of Deno version check."""
@@ -882,24 +992,41 @@ class UpdaterTabWidget(QWidget):
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
) )
# Run update in background thread # Use QThread for updates with progress reporting
def update_thread(): self.deno_update_thread = DenoUpdateThread()
try: self.deno_update_thread.progress.connect(self._on_deno_update_progress)
success, output = upgrade_deno() self.deno_update_thread.finished.connect(self._on_deno_update_finished)
self.deno_update_thread.error.connect(self._on_deno_update_error)
self.deno_update_thread.start()
# Update UI in main thread @Slot(str)
def _on_deno_update_progress(self, message: str) -> None:
"""Handle Deno update progress messages."""
# Clean up message for display
# Strip ANSI escape codes (colors)
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
display_msg = ansi_escape.sub('', message).strip()
if not display_msg:
return
# If message is too long, truncate it
if len(display_msg) > 70:
display_msg = display_msg[:67] + "..."
self.deno_status_label.setText(display_msg)
@Slot(bool, str)
def _on_deno_update_finished(self, success: bool, output: str) -> None:
self.deno_check_button.setEnabled(True) self.deno_check_button.setEnabled(True)
self.deno_update_button.setEnabled(True) self.deno_update_button.setEnabled(True)
self._handle_deno_update_result(success, output) self._handle_deno_update_result(success, output)
except Exception as e: @Slot(str)
logger.exception(f"Error updating Deno: {e}") def _on_deno_update_error(self, error: str) -> None:
self.deno_check_button.setEnabled(True) self.deno_check_button.setEnabled(True)
self.deno_update_button.setEnabled(True) self.deno_update_button.setEnabled(True)
self._handle_deno_update_result(False, str(e)) self._handle_deno_update_result(False, error)
thread = threading.Thread(target=update_thread, daemon=True)
thread.start()
def _handle_deno_update_result(self, success: bool, output: str) -> None: def _handle_deno_update_result(self, success: bool, output: str) -> None:
"""Handle Deno update completion.""" """Handle Deno update completion."""
@@ -4,10 +4,10 @@ from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtGui import QColor, QFontMetrics from PySide6.QtGui import QColor, QFontMetrics
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget
from src.utils.ytsage_localization import _ from ..utils.ytsage_localization import _
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp from .ytsage_gui_main import YTSageApp
class FormatSignals(QObject): class FormatSignals(QObject):
@@ -158,6 +158,8 @@ class FormatTableMixin:
# Store format checkboxes and formats # Store format checkboxes and formats
self.format_checkboxes = [] self.format_checkboxes = []
self.all_formats = [] self.all_formats = []
self._row_format_type = [] # Track format type per row: 'video' or 'audio'
self._table_built = False # Track if table has been built with current formats
# Set table size policies # Set table size policies
self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
@@ -173,29 +175,42 @@ class FormatTableMixin:
def filter_formats(self) -> None: def filter_formats(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
if not hasattr(self, "all_formats"): if not hasattr(self, "all_formats") or not self.all_formats:
return return
# Check if we need to rebuild the table (first time or formats changed)
if not self._table_built:
self._build_full_format_table()
return
# Use row visibility for fast filtering instead of rebuilding table
show_video = hasattr(self, "video_button") and self.video_button.isChecked() # type: ignore[reportAttributeAccessIssue]
show_audio = hasattr(self, "audio_button") and self.audio_button.isChecked() # type: ignore[reportAttributeAccessIssue]
for row, format_type in enumerate(self._row_format_type):
if format_type == "video":
self.format_table.setRowHidden(row, not show_video)
else: # audio
self.format_table.setRowHidden(row, not show_audio)
def _build_full_format_table(self) -> None:
"""Build the complete format table once with all formats."""
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Clear current table # Clear current table
self.format_table.setRowCount(0) self.format_table.setRowCount(0)
self.format_checkboxes.clear() self.format_checkboxes.clear()
self._row_format_type.clear()
# Determine which formats to show # Separate and filter formats
filtered_formats = [] video_formats = [f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None]
audio_formats = [
if hasattr(self, "video_button") and self.video_button.isChecked(): # type: ignore[reportAttributeAccessIssue]
filtered_formats.extend([f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None])
if hasattr(self, "audio_button") and self.audio_button.isChecked(): # type: ignore[reportAttributeAccessIssue]
filtered_formats.extend(
[
f f
for f in self.all_formats for f in self.all_formats
if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower()) if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower())
and f.get("acodec") != "none" and f.get("acodec") != "none"
and f.get("filesize") is not None and f.get("filesize") is not None
] ]
)
# Sort formats by quality # Sort formats by quality
def get_quality(f): def get_quality(f):
@@ -211,17 +226,30 @@ class FormatTableMixin:
else: else:
return f.get("abr", 0) return f.get("abr", 0)
filtered_formats.sort(key=get_quality, reverse=True) video_formats.sort(key=get_quality, reverse=True)
audio_formats.sort(key=get_quality, reverse=True)
# Update table with filtered formats # Combine: video first, then audio (maintains logical grouping)
self.format_signals.format_update.emit(filtered_formats) all_filtered = [(f, "video") for f in video_formats] + [(f, "audio") for f in audio_formats]
def _update_format_table(self, formats) -> None: # Build table with format type tracking
self.format_table.setVisible(False)
self._populate_format_table(all_filtered)
self._table_built = True
# Apply initial visibility based on current button states
self.filter_formats()
# Animate table appearance
if hasattr(self, "animate_widget_fade_in"):
self.animate_widget_fade_in(self.format_table)
else:
self.format_table.setVisible(True)
def _populate_format_table(self, formats_with_types: list) -> None:
"""Populate the format table with formats and their types."""
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
self.format_table.setRowCount(0)
self.format_checkboxes.clear()
is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue] is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue]
# Configure columns based on mode # Configure columns based on mode
@@ -252,119 +280,103 @@ class FormatTableMixin:
_("formats.hdr"), _("formats.hdr"),
] ]
self.format_table.setHorizontalHeaderLabels(header_labels) self.format_table.setHorizontalHeaderLabels(header_labels)
# Ensure all columns are visible
for i in range(2, 9):
self.format_table.setColumnHidden(i, False)
# Apply responsive column widths for normal mode
self._apply_column_widths(header_labels, is_playlist_mode=False) self._apply_column_widths(header_labels, is_playlist_mode=False)
for f, format_type in formats_with_types:
for f in formats:
row = self.format_table.rowCount() row = self.format_table.rowCount()
self.format_table.insertRow(row) self.format_table.insertRow(row)
self._row_format_type.append(format_type)
# Column 0: Select Checkbox (Always shown) # Create checkbox widget
checkbox = QCheckBox() checkbox = QCheckBox()
checkbox.format_id = str(f.get("format_id", "")) # type: ignore[reportAttributeAccessIssue] checkbox.setStyleSheet("QCheckBox { margin-left: 8px; }")
checkbox.is_audio_only = bool((f.get("vcodec") or "none").lower() == "none") # type: ignore[attr-defined] checkbox.format_id = f["format_id"]
checkbox.has_audio = bool(f.get("acodec") and f.get("acodec") != "none") # type: ignore[attr-defined] checkbox.is_audio_only = f.get("vcodec") == "none"
checkbox.has_audio = f.get("acodec") != "none"
checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb)) checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb))
self.format_checkboxes.append(checkbox) self.format_checkboxes.append(checkbox)
checkbox_widget = QWidget()
checkbox_widget.setStyleSheet("background-color: transparent;") # Create a container widget for the checkbox
checkbox_layout = QHBoxLayout(checkbox_widget) checkbox_container = QWidget()
checkbox_layout = QHBoxLayout(checkbox_container)
checkbox_layout.addWidget(checkbox) checkbox_layout.addWidget(checkbox)
checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
checkbox_layout.setContentsMargins(0, 0, 0, 0) checkbox_layout.setContentsMargins(0, 0, 0, 0)
checkbox_layout.setSpacing(0) self.format_table.setCellWidget(row, 0, checkbox_container)
self.format_table.setCellWidget(row, 0, checkbox_widget)
# Column 1: Quality (Always shown) # Quality label with color coding
quality_text = self.get_quality_label(f) quality_label = self.get_quality_label(f)
quality_item = QTableWidgetItem(quality_text) quality_item = QTableWidgetItem(quality_label)
# Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms) # Set color based on quality (check multiple language terms)
quality_lower = quality_text.lower() # Make comparison case-insensitive quality_lower = quality_label.lower()
if any(term.lower() in quality_lower for term in ["Best", "Óptima", "Mejor", "Melhor", "Лучшее", "最佳", "Beste", "Meilleure", "सर्वोत्तम", "Terbaik", "En iyi", "Najlepsza", "Najlepszy", "Najlepsze", "Migliore", "Miglior", "الأفضل", "أفضل", "最高"]): if any(term.lower() in quality_lower for term in ["Best", "Óptima", "Mejor", "Melhor", "Лучшее", "最佳", "Beste", "Meilleure", "सर्वोत्तम", "Terbaik", "En iyi", "Najlepsza", "Najlepszy", "Najlepsze", "Migliore", "Miglior", "الأفضل", "أفضل", "最高"]):
quality_item.setForeground(QColor("#00ff00")) # Green for best quality quality_item.setForeground(QColor("#00ff00")) # Green for best quality
elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "Áudio Alto", "Audio Alto", "Высокое", "高清", "高质量", "Hoch", "Haute", "Élevé", "Audio élevé", "उच्च", "उच्च ऑडियो", "Tinggi", "Audio tinggi", "Yüksek", "Yüksek ses", "Wysoka", "Wysoki", "Wysokie", "Alta", "Audio alto", "عالية", "عالي", "صوت عالي", "", "高音質"]): elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "Áudio Alto", "Audio Alto", "Высокое", "高清", "高质量", "Hoch", "Haute", "Élevé", "Audio élevé", "उच्च", "उच्च ऑडियो", "Tinggi", "Audio tinggi", "Yüksek", "Yüksek ses", "Wysoka", "Wysoki", "Wysokie", "Audio alto", "عالية", "عالي", "صوت عالي", "", "高音質"]):
quality_item.setForeground(QColor("#00cc00")) # Light green for high quality quality_item.setForeground(QColor("#00cc00")) # Light green for high quality
elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "Média", "Áudio Médio", "Audio Medio", "Среднее", "中等", "Mittel", "Moyenne", "Audio moyen", "मध्यम", "मध्यम ऑडियो", "Sedang", "Audio sedang", "Orta", "Orta ses", "Średnia", "Średni", "Średnie", "Media", "Audio medio", "متوسطة", "متوسط", "صوت متوسط", "", "中音質"]): elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "Média", "Áudio Médio", "Audio Medio", "Среднее", "中等", "Mittel", "Moyenne", "Audio moyen", "मध्यम", "मध्यम ऑडियो", "Sedang", "Audio sedang", "Orta", "Orta ses", "Średnia", "Średni", "Średnie", "Audio medio", "متوسطة", "متوسط", "صوت متوسط", "", "中音質"]):
quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality
elif any(term.lower() in quality_lower for term in ["Low", "Baja", "Bajo", "Baixa", "Áudio Baixo", "Audio Bajo", "Низкое", "低质量", "Niedrig", "Niedriges Audio", "Faible", "Audio faible", "Qualité faible", "निम्न", "निम्न ऑडियो", "निम्न गुणवत्ता", "Rendah", "Audio rendah", "Kualitas rendah", "Düşük", "Düşük ses", "Düşük kalite", "Niska", "Niski", "Niskie", "Bassa", "Audio basso", "Bassa qualità", "منخفضة", "منخفض", "صوت منخفض", "جودة منخفضة", "", "低音質", "低品質"]): elif any(term.lower() in quality_lower for term in ["Low", "Baja", "Bajo", "Baixa", "Áudio Baixo", "Audio Bajo", "Низкое", "低质量", "Niedrig", "Niedriges Audio", "Faible", "Audio faible", "Qualité faible", "निम्न", "निम्न ऑडियो", "निम्न गुणवत्ता", "Rendah", "Audio rendah", "Kualitas rendah", "Düşük", "Düşük ses", "Düşük kalite", "Niska", "Niski", "Niskie", "Bassa", "Audio basso", "Bassa qualità", "منخفضة", "منخفض", "صوت منخفض", "جودة منخفضة", "", "低音質", "低品質"]):
quality_item.setForeground(QColor("#ff5555")) # Red for low quality quality_item.setForeground(QColor("#ff5555")) # Red for low quality
self.format_table.setItem(row, 1, quality_item) self.format_table.setItem(row, 1, quality_item)
# --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) --- # Resolution
# Column 2: Resolution (Always shown)
resolution = f.get("resolution", "N/A") resolution = f.get("resolution", "N/A")
if f.get("vcodec") == "none":
resolution = _("formats.audio_only_resolution") if is_playlist_mode:
# Column 2 for playlist mode: Resolution
self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
# Column 3: FPS for playlist mode, Extension for normal mode # Column 3: FPS (Frame Rate)
if is_playlist_mode:
# Get FPS for playlist mode
fps_value = f.get("fps") fps_value = f.get("fps")
if fps_value is not None: if fps_value is not None and fps_value >= 1:
# Format FPS value appropriately
if fps_value >= 1:
fps_text = f"{fps_value:.0f}fps" fps_text = f"{fps_value:.0f}fps"
else:
fps_text = "N/A" # Very low fps like storyboards
else: else:
fps_text = "N/A" fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text) fps_item = QTableWidgetItem(fps_text)
# Color code based on FPS value
if fps_value and fps_value >= 60: if fps_value and fps_value >= 60:
fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps fps_item.setForeground(QColor("#00ff00"))
elif fps_value and fps_value >= 30: elif fps_value and fps_value >= 30:
fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps fps_item.setForeground(QColor("#ffaa00"))
elif fps_value and fps_value >= 1: elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555")) # Red for low fps fps_item.setForeground(QColor("#ff5555"))
else: else:
fps_item.setForeground(QColor("#888888")) # Gray for N/A fps_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 3, fps_item) self.format_table.setItem(row, 3, fps_item)
# Column 4: HDR for playlist mode # Column 4: HDR
if f.get("vcodec") == "none": if f.get("vcodec") == "none":
# Audio-only formats don't have HDR
hdr_text = "N/A" hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for N/A hdr_item.setForeground(QColor("#888888"))
else: else:
hdr_value = f.get("dynamic_range") hdr_value = f.get("dynamic_range")
if hdr_value and hdr_value != "SDR": if hdr_value and hdr_value != "SDR":
hdr_text = hdr_value hdr_text = hdr_value
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR hdr_item.setForeground(QColor("#00ffff"))
else: else:
hdr_text = "SDR" hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for SDR hdr_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 4, hdr_item) self.format_table.setItem(row, 4, hdr_item)
else: else:
# Extension for normal mode (column 2) # Extension for normal mode (column 2)
self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper())) self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper()))
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status # Audio Status column
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none"
audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only")) audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only"))
audio_item = QTableWidgetItem(audio_status) audio_item = QTableWidgetItem(audio_status)
if needs_audio: if needs_audio:
audio_item.setForeground(QColor("#ffa500")) audio_item.setForeground(QColor("#ffa500"))
elif audio_status == _("formats.audio_only"): elif audio_status == _("formats.audio_only"):
audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only audio_item.setForeground(QColor("#cccccc"))
else: # Has Audio (Video+Audio) else:
audio_item.setForeground(QColor("#00cc00")) # Green for included audio audio_item.setForeground(QColor("#00cc00"))
# Set item for correct column based on mode
audio_column_index = 5 if is_playlist_mode else 6 audio_column_index = 5 if is_playlist_mode else 6
self.format_table.setItem(row, audio_column_index, audio_item) self.format_table.setItem(row, audio_column_index, audio_item)
# --- Populate columns only shown in non-playlist mode --- # Populate columns only shown in non-playlist mode
if not is_playlist_mode: if not is_playlist_mode:
# Column 3: Resolution # Column 3: Resolution
self.format_table.setItem(row, 3, QTableWidgetItem(resolution)) self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
@@ -384,45 +396,47 @@ class FormatTableMixin:
# Column 7: FPS (Frame Rate) # Column 7: FPS (Frame Rate)
fps_value = f.get("fps") fps_value = f.get("fps")
if fps_value is not None: if fps_value is not None and fps_value >= 1:
# Format FPS value appropriately
if fps_value >= 1:
fps_text = f"{fps_value:.0f}fps" fps_text = f"{fps_value:.0f}fps"
else:
fps_text = "N/A" # Very low fps like storyboards
else: else:
fps_text = "N/A" fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text) fps_item = QTableWidgetItem(fps_text)
# Color code based on FPS value
if fps_value and fps_value >= 60: if fps_value and fps_value >= 60:
fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps fps_item.setForeground(QColor("#00ff00"))
elif fps_value and fps_value >= 30: elif fps_value and fps_value >= 30:
fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps fps_item.setForeground(QColor("#ffaa00"))
elif fps_value and fps_value >= 1: elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555")) # Red for low fps fps_item.setForeground(QColor("#ff5555"))
else: else:
fps_item.setForeground(QColor("#888888")) # Gray for N/A fps_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 7, fps_item) self.format_table.setItem(row, 7, fps_item)
# Column 8: HDR (Dynamic Range) # Column 8: HDR (Dynamic Range)
if f.get("vcodec") == "none": if f.get("vcodec") == "none":
# Audio-only formats don't have HDR
hdr_text = "N/A" hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for N/A hdr_item.setForeground(QColor("#888888"))
else: else:
hdr_value = f.get("dynamic_range") hdr_value = f.get("dynamic_range")
if hdr_value and hdr_value != "SDR": if hdr_value and hdr_value != "SDR":
hdr_text = hdr_value hdr_text = hdr_value
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR hdr_item.setForeground(QColor("#00ffff"))
else: else:
hdr_text = "SDR" hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for SDR hdr_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 8, hdr_item) self.format_table.setItem(row, 8, hdr_item)
def _update_format_table(self, formats) -> None:
"""Signal handler that triggers a full table rebuild when formats change."""
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Mark table as needing rebuild and trigger it
self._table_built = False
self._build_full_format_table()
def handle_checkbox_click(self, clicked_checkbox) -> None: def handle_checkbox_click(self, clicked_checkbox) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
@@ -446,6 +460,7 @@ class FormatTableMixin:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
self.all_formats = formats self.all_formats = formats
self._table_built = False # Reset flag to trigger rebuild with new formats
self.format_signals.format_update.emit(formats) self.format_signals.format_update.emit(formats)
def get_quality_label(self, format_info) -> str: def get_quality_label(self, format_info) -> str:
File diff suppressed because it is too large Load Diff
@@ -6,19 +6,39 @@ from typing import TYPE_CHECKING, cast
import requests import requests
from PIL import Image from PIL import Image
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QPixmap from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py from .ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py
SponsorBlockCategoryDialog, SponsorBlockCategoryDialog,
SubtitleSelectionDialog, SubtitleSelectionDialog,
) )
from src.utils.ytsage_localization import _ from ..utils.ytsage_localization import _
from src.utils.ytsage_logger import logger from ..utils.ytsage_logger import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp from .ytsage_gui_main import YTSageApp
class ThumbnailDownloadThread(QThread):
"""Thread to download thumbnail image asynchronously."""
finished = Signal(bytes)
error = Signal(str)
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
try:
response = requests.get(self.url, timeout=10)
if response.status_code == 200:
self.finished.emit(response.content)
else:
self.error.emit(f"HTTP Error: {response.status_code}")
except Exception as e:
self.error.emit(str(e))
class VideoInfoMixin: class VideoInfoMixin:
@@ -290,7 +310,7 @@ class VideoInfoMixin:
# removed extra logic for mapping to main_windows # removed extra logic for mapping to main_windows
merge_checkbox = getattr(self, "merge_subs_checkbox", None) merge_checkbox = getattr(self, "merge_subs_checkbox", None)
if dialog.exec(): # If user clicks OK if self.run_dialog_with_blur(dialog): # If user clicks OK
self.selected_subtitles = dialog.get_selected_subtitles() self.selected_subtitles = dialog.get_selected_subtitles()
logger.info(f"Selected subtitles: {self.selected_subtitles}") logger.info(f"Selected subtitles: {self.selected_subtitles}")
# Update UI to reflect selection # Update UI to reflect selection
@@ -336,7 +356,7 @@ class VideoInfoMixin:
dialog = SponsorBlockCategoryDialog(dialog_categories, self) dialog = SponsorBlockCategoryDialog(dialog_categories, self)
if dialog.exec(): if self.run_dialog_with_blur(dialog):
self.selected_sponsorblock_categories = dialog.get_selected_categories() self.selected_sponsorblock_categories = dialog.get_selected_categories()
logger.info(f"SponsorBlock categories selected: {self.selected_sponsorblock_categories}") logger.info(f"SponsorBlock categories selected: {self.selected_sponsorblock_categories}")
self._update_sponsorblock_display() self._update_sponsorblock_display()
@@ -368,14 +388,21 @@ class VideoInfoMixin:
def download_thumbnail(self, url) -> None: def download_thumbnail(self, url) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
try:
# Store both thumbnail URL and video URL # Store both thumbnail URL and video URL
self.thumbnail_url = url self.thumbnail_url = url
self.video_url = self.url_input.text() # Get actual video URL self.video_url = self.url_input.text() # Get actual video URL
# Download thumbnail but don't save yet # Create and start loader thread
response = requests.get(url) # Keep reference to avoid garbage collection
self.thumbnail_image = Image.open(BytesIO(response.content)) self.thumbnail_thread = ThumbnailDownloadThread(url)
self.thumbnail_thread.finished.connect(self._on_thumbnail_downloaded)
self.thumbnail_thread.error.connect(lambda e: logger.error(f"Error loading thumbnail: {e}"))
self.thumbnail_thread.start()
def _on_thumbnail_downloaded(self, content: bytes) -> None:
self = cast("YTSageApp", self)
try:
self.thumbnail_image = Image.open(BytesIO(content))
# Display thumbnail # Display thumbnail
image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS) image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS)
@@ -383,9 +410,18 @@ class VideoInfoMixin:
image.save(img_byte_arr, format="PNG") image.save(img_byte_arr, format="PNG")
pixmap = QPixmap() pixmap = QPixmap()
pixmap.loadFromData(img_byte_arr.getvalue()) pixmap.loadFromData(img_byte_arr.getvalue())
# Fade in the thumbnail
self.thumbnail_label.setVisible(False)
self.thumbnail_label.setPixmap(pixmap) self.thumbnail_label.setPixmap(pixmap)
if hasattr(self, "animate_widget_fade_in"):
self.animate_widget_fade_in(self.thumbnail_label)
else:
self.thumbnail_label.setVisible(True)
except Exception as e: except Exception as e:
logger.exception(f"Error loading thumbnail: {e}") logger.exception(f"Error processing thumbnail image: {e}")
def download_thumbnail_file(self, video_url, path) -> bool: def download_thumbnail_file(self, video_url, path) -> bool:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
+123
View File
@@ -0,0 +1,123 @@
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import (
QFrame,
QGraphicsOpacityEffect,
QLabel,
QStackedWidget,
QTabBar,
QVBoxLayout,
QWidget,
)
class FadingStackedWidget(QStackedWidget):
"""
A QStackedWidget that cross-fades between widgets.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.fade_duration = 300
self.fade_easing = QEasingCurve.Type.OutQuad
def setCurrentIndex(self, index):
curr_index = self.currentIndex()
if index == curr_index:
return
widget = self.widget(index)
curr_widget = self.widget(curr_index)
# If widget isn't visible or valid, just swap
if not self.isVisible() or not curr_widget:
super().setCurrentIndex(index)
return
# 1. Capture the current view (the "old" tab)
# Use grab() for simplicity and reliability in PySide6
pixmap = self.grab()
# 2. Create an overlay label to hold this "old" view
overlay = QLabel(self)
overlay.setPixmap(pixmap)
overlay.setGeometry(0, 0, self.width(), self.height())
overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) # Don't block clicks
overlay.show()
# 3. Switch the actual stack to the "new" view
super().setCurrentIndex(index)
# CRITICAL: Ensure overlay stays on top of the new widget
overlay.raise_()
# 4. Fade OUT the overlay, revealing the new view
effect = QGraphicsOpacityEffect(overlay)
overlay.setGraphicsEffect(effect)
anim = QPropertyAnimation(effect, b"opacity", overlay)
anim.setDuration(self.fade_duration)
anim.setStartValue(1.0)
anim.setEndValue(0.0)
anim.setEasingCurve(self.fade_easing)
# Cleanup when done
anim.finished.connect(lambda: self._cleanup(overlay))
# Keep reference to prevent GC
self._active_anim = anim
anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
def _cleanup(self, overlay):
overlay.hide()
overlay.deleteLater()
class SmoothTabWidget(QWidget):
"""
A unified Widget that behaves like a QTabWidget but uses smooth fading transitions.
Includes a QTabBar and a FadingStackedWidget.
"""
def __init__(self, parent=None):
super().__init__(parent)
# Main Layout
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Tab Bar
self.tab_bar = QTabBar(self)
self.tab_bar.setDrawBase(False) # We draw border on content instead
self.tab_bar.currentChanged.connect(self.set_current_index)
self.layout.addWidget(self.tab_bar)
# Content Area (Frame) - Mimics QTabWidget::pane
self.content_frame = QFrame(self)
self.content_frame.setObjectName("tabContent")
# Layout inside the content frame
self.content_layout = QVBoxLayout(self.content_frame)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_layout.setSpacing(0)
# The Stack
self.stack = FadingStackedWidget(self.content_frame)
self.content_layout.addWidget(self.stack)
self.layout.addWidget(self.content_frame)
def addTab(self, widget, label):
"""Add a tab with the given widget and label."""
self.stack.addWidget(widget)
self.tab_bar.addTab(label)
def set_current_index(self, index):
"""Slot to handle tab bar clicks."""
self.tab_bar.setCurrentIndex(index)
self.stack.setCurrentIndex(index)
def currentWidget(self):
return self.stack.currentWidget()
def currentIndex(self):
return self.stack.currentIndex()
+432
View File
@@ -0,0 +1,432 @@
class StyleSheet:
MAIN = """
QMainWindow {
background-color: #15181b;
}
QWidget {
background-color: #15181b;
color: #ffffff;
}
QLineEdit {
padding: 5px 15px;
border: 2px solid #2a2d2e;
border-radius: 6px;
background-color: #1b2021;
color: #ffffff;
font-size: 13px;
}
QLineEdit:focus {
border-color: #ff6b6b;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
padding: 10px 13px 6px 17px;
}
QPushButton:disabled {
background-color: #3d3d3d;
color: #888888;
}
QTableWidget {
border: 2px solid #1b2021;
border-radius: 4px;
background-color: #1b2021;
gridline-color: #1b2021;
}
QHeaderView::section {
background-color: #15181b;
padding: 5px;
border: 1px solid #1b2021;
color: #ffffff;
}
QProgressBar {
border: 2px solid #1b2021;
border-radius: 4px;
text-align: center;
color: white;
}
QProgressBar::chunk {
background-color: #c90000;
border-radius: 2px;
}
QLabel {
color: #ffffff;
}
/* Style for filter buttons */
QPushButton.filter-btn {
background-color: #1b2021;
padding: 5px 10px;
margin: 0 5px;
}
QPushButton.filter-btn:checked {
background-color: #c90000;
}
QPushButton.filter-btn:hover {
background-color: #444444;
}
QPushButton.filter-btn:checked:hover {
background-color: #a50000;
}
/* Modern Scrollbar Styling */
QScrollBar:vertical {
border: none;
background: #15181b;
width: 14px;
margin: 15px 0 15px 0;
border-radius: 7px;
}
QScrollBar::handle:vertical {
background: #404040;
min-height: 30px;
border-radius: 7px;
}
QScrollBar::handle:vertical:hover {
background: #505050;
}
QScrollBar::sub-line:vertical {
border: none;
background: #15181b;
height: 15px;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:vertical {
border: none;
background: #15181b;
height: 15px;
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical:hover,
QScrollBar::add-line:vertical:hover {
background: #404040;
}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
background: none;
width: 0;
height: 0;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: none;
}
"""
PASTE_BUTTON = """
QPushButton {
padding: 9px 20px;
background-color: #1b2021;
border: 2px solid #2a2d2e;
border-radius: 5px;
color: #ffffff;
font-weight: 600;
font-size: 13px;
}
QPushButton:hover {
background-color: #252829;
border-color: #3a3d3e;
}
QPushButton:pressed {
background-color: #1a1d1e;
padding: 11px 18px 7px 22px;
}
"""
ANALYZE_BUTTON = """
QPushButton {
padding: 9px 20px;
background-color: #c90000;
border: none;
border-radius: 5px;
color: white;
font-weight: 600;
font-size: 13px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
padding: 11px 18px 7px 22px;
}
QPushButton:disabled {
background-color: #3d3d3d;
color: #888888;
}
"""
PLAYLIST_BUTTON = """
QPushButton {
padding: 6px 12px;
background-color: #1d1e22;
border: 1px solid #c90000;
border-radius: 4px;
color: white;
font-weight: normal;
text-align: left;
padding-left: 10px;
}
QPushButton:hover {
background-color: #2a2d36;
border-color: #a50000;
}
QPushButton:pressed {
background-color: #1d1e22;
padding: 8px 10px 4px 12px;
}
"""
FORMAT_TOGGLE_BUTTON = """
QPushButton {
padding: 8px 15px;
background-color: #1d1e22;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:checked {
background-color: #c90000;
}
QPushButton:hover {
background-color: #2a2d36;
}
QPushButton:checked:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #151619;
padding: 10px 13px 6px 17px;
}
QPushButton:checked:pressed {
background-color: #800000;
padding: 10px 13px 6px 17px;
}
"""
CHECKBOX = """
QCheckBox {
color: #ffffff;
padding: 5px;
margin-left: 20px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
QCheckBox:disabled { color: #888888; }
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
"""
PROGRESS_BAR = """
QProgressBar {
border: 2px solid #3d3d3d;
border-radius: 4px;
text-align: center;
color: white;
background-color: #363636;
height: 25px;
}
QProgressBar::chunk {
background-color: #ff0000;
border-radius: 2px;
}
"""
STATUS_LABEL = """
QLabel {
color: #cccccc;
font-size: 12px;
padding: 5px;
}
"""
OPEN_FOLDER_BUTTON = """
QPushButton {
background-color: #2a2d2e;
color: #cccccc;
border: 1px solid #404040;
border-radius: 5px;
font-size: 16px;
padding: 2px;
}
QPushButton:hover {
background-color: #3a3d3e;
border: 1px solid #505050;
}
QPushButton:pressed {
background-color: #1a1d1e;
padding: 4px 0px 0px 4px;
}
"""
UPDATE_DIALOG_MESSAGE = """
QLabel {
background-color: #1d1e22;
border: 1px solid #3d3d3d;
border-radius: 6px;
padding: 15px;
margin: 5px 0;
}
"""
UPDATE_DIALOG_CHANGELOG = """
QTextEdit {
background-color: #1d1e22;
border: 2px solid #3d3d3d;
border-radius: 6px;
color: #ffffff;
padding: 10px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 12px;
line-height: 1.4;
}
QScrollBar:vertical {
border: none;
background: #1d1e22;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background: #404040;
min-height: 20px;
border-radius: 6px;
}
QScrollBar::handle:vertical:hover {
background: #505050;
}
"""
UPDATE_DIALOG_DOWNLOAD_BTN = """
QPushButton {
padding: 10px 20px;
background-color: #c90000;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-width: 140px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
"""
UPDATE_DIALOG_REMIND_BTN = """
QPushButton {
padding: 10px 20px;
background-color: #3d3d3d;
border: 1px solid #555555;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-width: 140px;
}
QPushButton:hover {
background-color: #4d4d4d;
border-color: #666666;
}
QPushButton:pressed {
background-color: #2d2d2d;
}
"""
UPDATE_DIALOG_MAIN = """
QDialog {
background-color: #15181b;
border: 1px solid #3d3d3d;
border-radius: 8px;
}
QLabel {
color: #ffffff;
font-size: 12px;
}
"""
TIME_RANGE_BTN_ACTIVE = """
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
border: 2px solid white;
}
QPushButton:hover {
background-color: #a50000;
}
"""
FILE_EXISTS_DIALOG = """
QMessageBox {
background-color: #2b2b2b;
}
QLabel {
color: #ffffff;
}
QPushButton {
padding: 8px 15px;
background-color: #ff0000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #cc0000;
}
"""
SETUP_SUCCESS_DIALOG = """
QMessageBox {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
"""
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "صوت فقط" "audio_only_resolution": "صوت فقط"
}, },
"buttons": { "buttons": {
"reset": "إعادة تعيين",
"download": "تنزيل", "download": "تنزيل",
"pause": "إيقاف مؤقت", "pause": "إيقاف مؤقت",
"resume": "استئناف", "resume": "استئناف",
@@ -93,7 +94,8 @@
"select_subtitles": "اختر الترجمات", "select_subtitles": "اختر الترجمات",
"filter_languages_placeholder": "تصفية اللغات (مثال: ar، en)...", "filter_languages_placeholder": "تصفية اللغات (مثال: ar، en)...",
"no_subtitles_available": "لا توجد ترجمات متاحة", "no_subtitles_available": "لا توجد ترجمات متاحة",
"matching": "مطابقة" "matching": "مطابقة",
"ytdlp_log_title": "سجل yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "تسجيل الدخول بالكوكيز", "cookies": "تسجيل الدخول بالكوكيز",
@@ -124,7 +126,10 @@
"browser_selected_title": "تم تطبيق كوكيز المتصفح", "browser_selected_title": "تم تطبيق كوكيز المتصفح",
"browser_applied_message": "سيتم استخراج كوكيز المتصفح من: {browser}", "browser_applied_message": "سيتم استخراج كوكيز المتصفح من: {browser}",
"cleared_title": "تم مسح الكوكيز", "cleared_title": "تم مسح الكوكيز",
"cleared_message": "تم مسح إعدادات الكوكيز" "cleared_message": "تم مسح إعدادات الكوكيز",
"active_browser": "✓ نشط: كوكيز المتصفح ({browser})",
"active_file": "✓ نشط: ملف كوكيز ({file})",
"none_active": "○ لا توجد كوكيز نشطة"
}, },
"custom_command": { "custom_command": {
"help_text": "أدخل أمر yt-dlp مخصص أدناه. سيتم إضافة الرابط الحالي تلقائياً.<br><br>للحصول على قائمة كاملة بالخيارات وأمثلة الاستخدام <a href=\"{docs_url}\">انقر هنا لمشاهدة الوثائق الرسمية لـ yt-dlp</a>.<br><br>ملاحظة: يتم التعامل مع مسار التنزيل ونموذج اسم الملف تلقائياً.", "help_text": "أدخل أمر yt-dlp مخصص أدناه. سيتم إضافة الرابط الحالي تلقائياً.<br><br>للحصول على قائمة كاملة بالخيارات وأمثلة الاستخدام <a href=\"{docs_url}\">انقر هنا لمشاهدة الوثائق الرسمية لـ yt-dlp</a>.<br><br>ملاحظة: يتم التعامل مع مسار التنزيل ونموذج اسم الملف تلقائياً.",
@@ -137,7 +142,14 @@
"full_command": "🔧 الأمر الكامل: {command}", "full_command": "🔧 الأمر الكامل: {command}",
"command_success": "✅ تم تنفيذ الأمر المخصص بنجاح!", "command_success": "✅ تم تنفيذ الأمر المخصص بنجاح!",
"command_failed": "❌ فشل الأمر برمز الخروج {code}", "command_failed": "❌ فشل الأمر برمز الخروج {code}",
"command_error": "❌ خطأ في تنفيذ الأمر المخصص: {error}" "command_error": "❌ خطأ في تنفيذ الأمر المخصص: {error}",
"error_no_url": "❌ خطأ: لم يتم تقديم عنوان URL. يرجى إدخال رابط في النافذة الرئيسية.",
"error_no_command": "❌ خطأ: لم يتم تقديم أي أمر. يرجى إدخال معاملات yt-dlp.",
"executing": "🚀 جارٍ تنفيذ أمر yt-dlp مخصص",
"url_label": "📍 الرابط: {url}",
"args_label": "⚙️ المعاملات: {command}",
"download_path_label": "📁 مسار التنزيل: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "قم بتكوين إعدادات البروكسي للتنزيلات. اتركه فارغاً للاتصال المباشر.", "help_text": "قم بتكوين إعدادات البروكسي للتنزيلات. اتركه فارغاً للاتصال المباشر.",
@@ -159,7 +171,15 @@
"invalid_main_url": "تنسيق رابط البروكسي الرئيسي غير صالح", "invalid_main_url": "تنسيق رابط البروكسي الرئيسي غير صالح",
"invalid_geo_url": "تنسيق رابط بروكسي الموقع غير صالح", "invalid_geo_url": "تنسيق رابط بروكسي الموقع غير صالح",
"main_configured": "تم تكوين البروكسي الرئيسي", "main_configured": "تم تكوين البروكسي الرئيسي",
"geo_configured": "تم تكوين بروكسي الموقع" "geo_configured": "تم تكوين بروكسي الموقع",
"set_title": "تم تعيين الوكيل",
"set_message": "تم تعيين الوكيل الرئيسي وحفظه: {proxy}",
"geo_set_title": "تم تعيين وكيل جغرافي",
"geo_set_message": "تم تعيين وكيل التحقق الجغرافي وحفظه: {proxy}",
"cleared_title": "تم مسح إعدادات الوكيل",
"cleared_message": "تم مسح جميع إعدادات الوكيل وحفظها.",
"saved_main": "تم حفظ الوكيل الرئيسي: {proxy}",
"saved_geo": "تم حفظ وكيل الموقع: {proxy}"
}, },
"download": { "download": {
"preparing": "جاري التحضير للتنزيل...", "preparing": "جاري التحضير للتنزيل...",
@@ -225,6 +245,8 @@
"system_info": "معلومات النظام", "system_info": "معلومات النظام",
"loading": "🔄 جاري تحميل معلومات النظام...", "loading": "🔄 جاري تحميل معلومات النظام...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 السجلات",
"logs_tooltip": "فتح مجلد سجلات التطبيق",
"refreshing": "🔄 جاري التحديث...", "refreshing": "🔄 جاري التحديث...",
"refresh_failed": "فشل التحديث", "refresh_failed": "فشل التحديث",
"refresh_failed_message": "تعذر تحديث معلومات الإصدار.", "refresh_failed_message": "تعذر تحديث معلومات الإصدار.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ تم التبديل بنجاح إلى قناة {channel}!", "ytdlp_channel_switched": "✅ تم التبديل بنجاح إلى قناة {channel}!",
"ytdlp_channel_switch_failed": "❌ فشل تبديل القناة: {error}", "ytdlp_channel_switch_failed": "❌ فشل تبديل القناة: {error}",
"ytdlp_current_channel": "القناة الحالية: {channel}", "ytdlp_current_channel": "القناة الحالية: {channel}",
"app_updates_title": "تحديثات YTSage",
"check_beta_updates": "تلقي تحديثات تجريبية (Beta)",
"auto_update_title": "إعدادات التحديثات التلقائية", "auto_update_title": "إعدادات التحديثات التلقائية",
"auto_update_header": "🔄 إعدادات التحديثات التلقائية", "auto_update_header": "🔄 إعدادات التحديثات التلقائية",
"auto_update_description": "قم بتكوين التحديثات التلقائية لـ yt-dlp لضمان الحصول على أحدث الميزات وإصلاحات الأخطاء.", "auto_update_description": "قم بتكوين التحديثات التلقائية لـ yt-dlp لضمان الحصول على أحدث الميزات وإصلاحات الأخطاء.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (غير مضغوط)", "audio_format_wav": "WAV (غير مضغوط)",
"audio_format_opus": "Opus (فعال)", "audio_format_opus": "Opus (فعال)",
"audio_format_m4a": "M4A (أبل)", "audio_format_m4a": "M4A (أبل)",
"audio_format_vorbis": "Vorbis (مفتوح)" "audio_format_vorbis": "Vorbis (مفتوح)",
"filename_format": "تنسيق اسم الملف الناتج",
"filename_format_help": "المتغيرات المتاحة: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. يتم دعم صيغة قالب إخراج yt-dlp القياسية."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "أدخل رابط فيديو يوتيوب أو قائمة تشغيل", "url_placeholder": "أدخل رابط فيديو يوتيوب أو قائمة تشغيل",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "سيتم استخراج كوكيز المتصفح من: {browser}", "browser_cookies_selected_message": "سيتم استخراج كوكيز المتصفح من: {browser}",
"error_no_format_info": "خطأ: لا توجد معلومات تنسيق متاحة.", "error_no_format_info": "خطأ: لا توجد معلومات تنسيق متاحة.",
"error_extract_info": "خطأ: تعذر استخراج معلومات الفيديو الأساسية. تحقق من الرابط.", "error_extract_info": "خطأ: تعذر استخراج معلومات الفيديو الأساسية. تحقق من الرابط.",
"analyzing_preparing": "التحليل (0%)... جاري التحضير للطلب", "analyzing_preparing": "جاري التحضير للطلب...",
"analyzing_extracting_basic": "التحليل (15%)... جاري استخراج المعلومات الأساسية", "analyzing_extracting_basic": "جاري استخراج المعلومات الأساسية...",
"analyzing_extracting_detailed": "التحليل (30%)... جاري استخراج المعلومات التفصيلية", "analyzing_extracting_detailed": "جاري استخراج المعلومات التفصيلية...",
"analyzing_processing_video": "التحليل (45%)... جاري معالجة بيانات الفيديو", "analyzing_processing_video": "جاري معالجة بيانات الفيديو...",
"analyzing_processing_formats": "التحليل (60%)... جاري معالجة التنسيقات", "analyzing_processing_formats": "جاري معالجة التنسيقات...",
"analyzing_loading_thumbnail": "التحليل (75%)... جاري تحميل الصورة المصغرة", "analyzing_loading_thumbnail": "جاري تحميل الصورة المصغرة...",
"analyzing_processing_subtitles": "التحليل (85%)... جاري معالجة الترجمات", "analyzing_processing_subtitles": "جاري معالجة الترجمات...",
"analyzing_updating_table": "التحليل (95%)... جاري تحديث جدول التنسيقات", "analyzing_updating_table": "جاري تحديث جدول التنسيقات...",
"analysis_complete": "اكتمل التحليل!", "analysis_complete": "اكتمل التحليل!",
"analyzing_extracting_ytdlp": "التحليل (30%)... جاري استخراج المعلومات", "analyzing_extracting_ytdlp": "جاري استخراج المعلومات...",
"analyzing_processing_data": "التحليل (60%)... جاري معالجة البيانات", "analyzing_fetching_first_video": "جاري جلب التنسيقات للفيديو الأول...",
"analyzing_processing_formats_ytdlp": "التحليل (75%)... جاري معالجة التنسيقات", "analyzing_processing_data": "جاري معالجة البيانات...",
"analyzing_loading_thumbnail_ytdlp": "التحليل (85%)... جاري تحميل الصورة المصغرة", "analyzing_processing_formats_ytdlp": "جاري معالجة التنسيقات...",
"analyzing_processing_subtitles_ytdlp": "التحليل (90%)... جاري معالجة الترجمات", "analyzing_loading_thumbnail_ytdlp": "جاري تحميل الصورة المصغرة...",
"analyzing_processing_subtitles_ytdlp": "جاري معالجة الترجمات...",
"select_subtitles": "اختر الترجمات...", "select_subtitles": "اختر الترجمات...",
"sponsorblock_categories": "فئات SponsorBlock...", "sponsorblock_categories": "فئات SponsorBlock...",
"invalid_url_or_enter": "عنوان URL غير صالح أو الرجاء إدخال عنوان URL.", "invalid_url_or_enter": "عنوان URL غير صالح أو الرجاء إدخال عنوان URL.",
"zero_selected": "تم اختيار 0", "zero_selected": "تم اختيار 0",
"analyze_first_tooltip": "يرجى تحليل الفيديو أولاً", "analyze_first_tooltip": "يرجى تحليل الفيديو أولاً",
"audio_mode_disabled": "غير متاح في وضع الصوت فقط", "audio_mode_disabled": "غير متاح في وضع الصوت فقط",
"select_subtitles_first": "يرجى تحديد الترجمة أولاً" "select_subtitles_first": "يرجى تحديد الترجمة أولاً",
"settings_tooltip": "المسار الحالي: {path}\nحد السرعة: {speed_limit}",
"speed_limit_none": "بدون",
"open_folder_error": "تعذر فتح المجلد: {error}",
"time_range_set": "تم تعيين المقطع: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "الراعي", "sponsor": "الراعي",
@@ -408,7 +439,10 @@
"ytdlp_failed": "خطأ: فشل yt-dlp: {error}", "ytdlp_failed": "خطأ: فشل yt-dlp: {error}",
"parse_failed": "خطأ: فشل تحليل مخرجات yt-dlp: {error}", "parse_failed": "خطأ: فشل تحليل مخرجات yt-dlp: {error}",
"analysis_failed": "خطأ: فشل التحليل: {error}", "analysis_failed": "خطأ: فشل التحليل: {error}",
"generic_error": "خطأ: {error}" "generic_error": "خطأ: {error}",
"download_failed_return_code_conflict": "فشل التنزيل برمز الإرجاع {return_code}. قد يكون ذلك بسبب تعارض بين عدة عمليات تثبيت لـ yt-dlp. جرّب إزالة أي تثبيت لنظام التشغيل (مثل snap أو apt) ثم أعد تشغيل التطبيق.",
"download_failed_return_code": "فشل التنزيل برمز الإرجاع {return_code}",
"direct_command_error": "خطأ في الأمر المباشر: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "تحديث متاح", "title": "تحديث متاح",
@@ -442,7 +476,8 @@
"next_check": "الفحص التالي: {time}", "next_check": "الفحص التالي: {time}",
"next_check_error": "الفحص التالي: خطأ في الحساب", "next_check_error": "الفحص التالي: خطأ في الحساب",
"checking": "🔄 جاري الفحص...", "checking": "🔄 جاري الفحص...",
"check_now": "🔍 التحقق من التحديثات الآن" "check_now": "🔍 التحقق من التحديثات الآن",
"current_version": "إصدار yt-dlp الحالي: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "لا يمكن أن يكون الرابط فارغًا", "empty_url": "لا يمكن أن يكون الرابط فارغًا",
@@ -473,6 +508,7 @@
"clear_confirm_message": "هل أنت متأكد؟ لا يمكن التراجع عن هذا.", "clear_confirm_message": "هل أنت متأكد؟ لا يمكن التراجع عن هذا.",
"no_history": "لا يوجد سجل بعد", "no_history": "لا يوجد سجل بعد",
"no_history_description": "ستظهر تنزيلاتك هنا", "no_history_description": "ستظهر تنزيلاتك هنا",
"loading": "جارٍ تحميل السجل...",
"search_placeholder": "بحث...", "search_placeholder": "بحث...",
"open_location": "فتح الموقع", "open_location": "فتح الموقع",
"redownload": "إعادة التنزيل", "redownload": "إعادة التنزيل",
@@ -491,7 +527,47 @@
"one_entry": "تنزيل واحد", "one_entry": "تنزيل واحد",
"redownload_confirm_title": "إعادة التنزيل؟", "redownload_confirm_title": "إعادة التنزيل؟",
"redownload_confirm_message": "إعادة التنزيل؟\n\n{title}", "redownload_confirm_message": "إعادة التنزيل؟\n\n{title}",
"redownload_started": "بدأ التنزيل" "redownload_started": "بدأ التنزيل",
"no_url_error": "لم يتم العثور على عنوان URL في سجل التاريخ",
"redownload_failed": "فشل بدء إعادة التنزيل: {error}"
},
"ffmpeg": {
"installation_title": "تثبيت FFmpeg",
"installation_message": "يحتاج YTSage إلى FFmpeg لمعالجة الفيديوهات.\n\nاختر خيار التثبيت أدناه:",
"install_button": "تثبيت FFmpeg",
"manual_guide": "الدليل اليدوي",
"installation_failed": "واجه تثبيت FFmpeg مشكلة.",
"already_installed": "تم تثبيت FFmpeg بالفعل!",
"installation_complete": "اكتملت عملية التثبيت. يمكنك إغلاق هذا الحوار ومتابعة استخدام YTSage.",
"installing": "جارٍ تثبيت FFmpeg... يرجى الانتظار",
"install_success": "تم تثبيت FFmpeg بنجاح!",
"installation_complete_close": "اكتملت عملية التثبيت. يمكنك الآن إغلاق هذا الحوار ومتابعة استخدام YTSage.",
"try_manual": "يرجى محاولة استخدام دليل التثبيت اليدوي بدلاً من ذلك."
},
"ytdlp_setup": {
"required_title": "إعداد yt-dlp مطلوب",
"description": "يتطلب YTSage وجود yt-dlp لتنزيل الفيديوهات.<br><br>لم يتم العثور على yt-dlp في الدليل المحلي للتطبيق. يحتاج YTSage إلى إعداد yt-dlp لنظام {os_name}.<br><br>يرجى اختيار خيار أدناه:",
"option_auto": "تنزيل تلقائيًا (موصى به)",
"option_manual": "اختيار المسار يدويًا",
"setup_button": "إعداد yt-dlp",
"downloading": "جارٍ تنزيل yt-dlp...",
"success": "تم تثبيت yt-dlp بنجاح!",
"error": "خطأ: {error}",
"download_failed_title": "فشل التنزيل",
"download_failed_message": "فشل تنزيل yt-dlp: {error}",
"select_executable_title": "اختر ملف yt-dlp التنفيذي",
"copied_to": "تم نسخ yt-dlp بنجاح إلى {path}",
"setup_error_title": "خطأ في الإعداد",
"copy_error": "خطأ أثناء نسخ yt-dlp إلى مجلد التطبيق: {error}",
"invalid_executable_title": "ملف تنفيذي غير صالح",
"invalid_executable_message": "الملف المحدد لا يبدو كتنفيذ صحيح لـ yt-dlp.",
"verify_error": "خطأ أثناء التحقق من ملف yt-dlp التنفيذي: {error}",
"setup_failed_title": "فشل الإعداد",
"setup_failed_message": "فشل إعداد yt-dlp. قد لا تعمل بعض الميزات بشكل صحيح.",
"success_dialog_title": "إعداد yt-dlp",
"success_dialog_message": "تم تكوين yt-dlp بنجاح في:\n{path}",
"file_filter_windows": "ملفات تنفيذية (*.exe)",
"file_filter_all": "كل الملفات (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "مدقق إصدار FFmpeg", "title": "مدقق إصدار FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Nur Audio" "audio_only_resolution": "Nur Audio"
}, },
"buttons": { "buttons": {
"reset": "Zurücksetzen",
"download": "Herunterladen", "download": "Herunterladen",
"pause": "Pausieren", "pause": "Pausieren",
"resume": "Fortsetzen", "resume": "Fortsetzen",
@@ -93,7 +94,8 @@
"select_subtitles": "Untertitel auswählen", "select_subtitles": "Untertitel auswählen",
"filter_languages_placeholder": "Sprachen filtern (z.B. en, de)...", "filter_languages_placeholder": "Sprachen filtern (z.B. en, de)...",
"no_subtitles_available": "Keine Untertitel verfügbar", "no_subtitles_available": "Keine Untertitel verfügbar",
"matching": "passend" "matching": "passend",
"ytdlp_log_title": "yt-dlp-Protokoll"
}, },
"tabs": { "tabs": {
"cookies": "Mit Cookies anmelden", "cookies": "Mit Cookies anmelden",
@@ -124,7 +126,10 @@
"browser_selected_title": "Browser-Cookies angewendet", "browser_selected_title": "Browser-Cookies angewendet",
"browser_applied_message": "Browser-Cookies werden extrahiert von: {browser}", "browser_applied_message": "Browser-Cookies werden extrahiert von: {browser}",
"cleared_title": "Cookies gelöscht", "cleared_title": "Cookies gelöscht",
"cleared_message": "Cookie-Einstellungen wurden gelöscht" "cleared_message": "Cookie-Einstellungen wurden gelöscht",
"active_browser": "✓ Aktiv: Browser-Cookies ({browser})",
"active_file": "✓ Aktiv: Cookie-Datei ({file})",
"none_active": "○ Keine Cookies aktiv"
}, },
"custom_command": { "custom_command": {
"help_text": "Geben Sie Ihren benutzerdefinierten yt-dlp-Befehl unten ein. Die aktuelle URL wird automatisch angehängt.<br><br>Für die vollständige Liste der Optionen und Verwendungsbeispiele <a href=\"{docs_url}\">klicken Sie hier, um die offizielle yt-dlp-Dokumentation anzuzeigen</a>.<br><br>Hinweis: Download-Pfad und Dateinamen-Vorlage werden automatisch behandelt.", "help_text": "Geben Sie Ihren benutzerdefinierten yt-dlp-Befehl unten ein. Die aktuelle URL wird automatisch angehängt.<br><br>Für die vollständige Liste der Optionen und Verwendungsbeispiele <a href=\"{docs_url}\">klicken Sie hier, um die offizielle yt-dlp-Dokumentation anzuzeigen</a>.<br><br>Hinweis: Download-Pfad und Dateinamen-Vorlage werden automatisch behandelt.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Vollständiger Befehl: {command}", "full_command": "🔧 Vollständiger Befehl: {command}",
"command_success": "✅ Benutzerdefinierter Befehl erfolgreich ausgeführt!", "command_success": "✅ Benutzerdefinierter Befehl erfolgreich ausgeführt!",
"command_failed": "❌ Befehl fehlgeschlagen mit Exit-Code {code}", "command_failed": "❌ Befehl fehlgeschlagen mit Exit-Code {code}",
"command_error": "❌ Fehler beim Ausführen des benutzerdefinierten Befehls: {error}" "command_error": "❌ Fehler beim Ausführen des benutzerdefinierten Befehls: {error}",
"error_no_url": "❌ Fehler: Keine URL angegeben. Bitte geben Sie im Hauptfenster eine URL ein.",
"error_no_command": "❌ Fehler: Kein Befehl angegeben. Bitte yt-dlp-Argumente eingeben.",
"executing": "🚀 Benutzerdefinierter yt-dlp-Befehl wird ausgeführt",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argumente: {command}",
"download_path_label": "📁 Download-Pfad: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Proxy-Einstellungen für Downloads konfigurieren. Leer lassen für direkte Verbindung.", "help_text": "Proxy-Einstellungen für Downloads konfigurieren. Leer lassen für direkte Verbindung.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Ungültiges Haupt-Proxy-URL-Format", "invalid_main_url": "Ungültiges Haupt-Proxy-URL-Format",
"invalid_geo_url": "Ungültiges Geo-Proxy-URL-Format", "invalid_geo_url": "Ungültiges Geo-Proxy-URL-Format",
"main_configured": "Haupt-Proxy konfiguriert", "main_configured": "Haupt-Proxy konfiguriert",
"geo_configured": "Geo-Proxy konfiguriert" "geo_configured": "Geo-Proxy konfiguriert",
"set_title": "Proxy gesetzt",
"set_message": "Haupt-Proxy gesetzt und gespeichert: {proxy}",
"geo_set_title": "Geo-Proxy gesetzt",
"geo_set_message": "Geo-Prüf-Proxy gesetzt und gespeichert: {proxy}",
"cleared_title": "Proxy-Einstellungen gelöscht",
"cleared_message": "Alle Proxy-Einstellungen wurden gelöscht und gespeichert.",
"saved_main": "Gespeicherter Haupt-Proxy: {proxy}",
"saved_geo": "Gespeicherter Geo-Proxy: {proxy}"
}, },
"download": { "download": {
"preparing": "Download wird vorbereitet...", "preparing": "Download wird vorbereitet...",
@@ -225,6 +245,8 @@
"system_info": "Systeminformationen", "system_info": "Systeminformationen",
"loading": "🔄 Systeminformationen werden geladen...", "loading": "🔄 Systeminformationen werden geladen...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Logs",
"logs_tooltip": "Anwendungs-Protokollordner öffnen",
"refreshing": "🔄 Wird aktualisiert...", "refreshing": "🔄 Wird aktualisiert...",
"refresh_failed": "Aktualisierung fehlgeschlagen", "refresh_failed": "Aktualisierung fehlgeschlagen",
"refresh_failed_message": "Versionsinformationen konnten nicht aktualisiert werden.", "refresh_failed_message": "Versionsinformationen konnten nicht aktualisiert werden.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Erfolgreich zu {channel}-Kanal gewechselt!", "ytdlp_channel_switched": "✅ Erfolgreich zu {channel}-Kanal gewechselt!",
"ytdlp_channel_switch_failed": "❌ Kanalwechsel fehlgeschlagen: {error}", "ytdlp_channel_switch_failed": "❌ Kanalwechsel fehlgeschlagen: {error}",
"ytdlp_current_channel": "Aktueller Kanal: {channel}", "ytdlp_current_channel": "Aktueller Kanal: {channel}",
"app_updates_title": "YTSage-Updates",
"check_beta_updates": "Beta-Updates erhalten",
"auto_update_title": "Auto-Update-Einstellungen", "auto_update_title": "Auto-Update-Einstellungen",
"auto_update_header": "🔄 Auto-Update-Einstellungen", "auto_update_header": "🔄 Auto-Update-Einstellungen",
"auto_update_description": "Konfigurieren Sie automatische Updates für yt-dlp, um sicherzustellen, dass Sie immer die neuesten Funktionen und Fehlerbehebungen haben.", "auto_update_description": "Konfigurieren Sie automatische Updates für yt-dlp, um sicherzustellen, dass Sie immer die neuesten Funktionen und Fehlerbehebungen haben.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Unkomprimiert)", "audio_format_wav": "WAV (Unkomprimiert)",
"audio_format_opus": "Opus (Effizient)", "audio_format_opus": "Opus (Effizient)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Offen)" "audio_format_vorbis": "Vorbis (Offen)",
"filename_format": "Ausgabe-Dateinamenformat",
"filename_format_help": "Verfügbare Variablen: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Die Standard-yt-dlp-Ausgabevorlagensyntax wird unterstützt."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "YouTube-Video- oder Playlist-URL eingeben", "url_placeholder": "YouTube-Video- oder Playlist-URL eingeben",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "Browser-Cookies werden aus folgendem Browser extrahiert: {browser}", "browser_cookies_selected_message": "Browser-Cookies werden aus folgendem Browser extrahiert: {browser}",
"error_no_format_info": "Fehler: Keine Formatinformationen verfügbar.", "error_no_format_info": "Fehler: Keine Formatinformationen verfügbar.",
"error_extract_info": "Fehler: Grundlegende Videoinformationen konnten nicht extrahiert werden. Bitte überprüfen Sie Ihren Link.", "error_extract_info": "Fehler: Grundlegende Videoinformationen konnten nicht extrahiert werden. Bitte überprüfen Sie Ihren Link.",
"analyzing_preparing": "Analysiere (0%)... Anfrage wird vorbereitet", "analyzing_preparing": "Anfrage wird vorbereitet...",
"analyzing_extracting_basic": "Analysiere (15%)... Grundlegende Informationen werden extrahiert", "analyzing_extracting_basic": "Grundlegende Informationen werden extrahiert...",
"analyzing_extracting_detailed": "Analysiere (30%)... Detaillierte Informationen werden extrahiert", "analyzing_extracting_detailed": "Detaillierte Informationen werden extrahiert...",
"analyzing_processing_video": "Analysiere (45%)... Videodaten werden verarbeitet", "analyzing_processing_video": "Videodaten werden verarbeitet...",
"analyzing_processing_formats": "Analysiere (60%)... Formate werden verarbeitet", "analyzing_processing_formats": "Formate werden verarbeitet...",
"analyzing_loading_thumbnail": "Analysiere (75%)... Thumbnail wird geladen", "analyzing_loading_thumbnail": "Thumbnail wird geladen...",
"analyzing_processing_subtitles": "Analysiere (85%)... Untertitel werden verarbeitet", "analyzing_processing_subtitles": "Untertitel werden verarbeitet...",
"analyzing_updating_table": "Analysiere (95%)... Formattabelle wird aktualisiert", "analyzing_updating_table": "Formattabelle wird aktualisiert...",
"analysis_complete": "Analyse abgeschlossen!", "analysis_complete": "Analyse abgeschlossen!",
"analyzing_extracting_ytdlp": "Analysiere (30%)... Informationen werden extrahiert", "analyzing_extracting_ytdlp": "Informationen werden extrahiert...",
"analyzing_processing_data": "Analysiere (60%)... Daten werden verarbeitet", "analyzing_fetching_first_video": "Formate für das erste Video werden abgerufen...",
"analyzing_processing_formats_ytdlp": "Analysiere (75%)... Formate werden verarbeitet", "analyzing_processing_data": "Daten werden verarbeitet...",
"analyzing_loading_thumbnail_ytdlp": "Analysiere (85%)... Thumbnail wird geladen", "analyzing_processing_formats_ytdlp": "Formate werden verarbeitet...",
"analyzing_processing_subtitles_ytdlp": "Analysiere (90%)... Untertitel werden verarbeitet", "analyzing_loading_thumbnail_ytdlp": "Thumbnail wird geladen...",
"analyzing_processing_subtitles_ytdlp": "Untertitel werden verarbeitet...",
"select_subtitles": "Untertitel auswählen...", "select_subtitles": "Untertitel auswählen...",
"sponsorblock_categories": "SponsorBlock-Kategorien...", "sponsorblock_categories": "SponsorBlock-Kategorien...",
"invalid_url_or_enter": "Ungültige URL oder bitte geben Sie eine URL ein.", "invalid_url_or_enter": "Ungültige URL oder bitte geben Sie eine URL ein.",
"zero_selected": "0 ausgewählt", "zero_selected": "0 ausgewählt",
"analyze_first_tooltip": "Bitte analysieren Sie zuerst das Video", "analyze_first_tooltip": "Bitte analysieren Sie zuerst das Video",
"audio_mode_disabled": "Nicht verfügbar im Nur-Audio-Modus", "audio_mode_disabled": "Nicht verfügbar im Nur-Audio-Modus",
"select_subtitles_first": "Bitte wählen Sie zuerst Untertitel aus" "select_subtitles_first": "Bitte wählen Sie zuerst Untertitel aus",
"settings_tooltip": "Aktueller Pfad: {path}\nGeschwindigkeitslimit: {speed_limit}",
"speed_limit_none": "Keines",
"open_folder_error": "Ordner konnte nicht geöffnet werden: {error}",
"time_range_set": "Abschnitt gesetzt: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Fehler: yt-dlp fehlgeschlagen: {error}", "ytdlp_failed": "Fehler: yt-dlp fehlgeschlagen: {error}",
"parse_failed": "Fehler: Fehler beim Parsen der yt-dlp-Ausgabe: {error}", "parse_failed": "Fehler: Fehler beim Parsen der yt-dlp-Ausgabe: {error}",
"analysis_failed": "Fehler: Analyse fehlgeschlagen: {error}", "analysis_failed": "Fehler: Analyse fehlgeschlagen: {error}",
"generic_error": "Fehler: {error}" "generic_error": "Fehler: {error}",
"download_failed_return_code_conflict": "Download fehlgeschlagen mit Rückgabecode {return_code}. Dies kann an einem Konflikt mit mehreren yt-dlp-Installationen liegen. Deinstalliere ggf. eine systemweit installierte yt-dlp-Version (z. B. über snap oder apt) und starte die Anwendung neu.",
"download_failed_return_code": "Download fehlgeschlagen mit Rückgabecode {return_code}",
"direct_command_error": "Fehler im direkten Befehl: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Update verfügbar", "title": "Update verfügbar",
@@ -442,7 +476,8 @@
"next_check": "Nächste Prüfung: {time}", "next_check": "Nächste Prüfung: {time}",
"next_check_error": "Nächste Prüfung: Fehler bei Berechnung", "next_check_error": "Nächste Prüfung: Fehler bei Berechnung",
"checking": "🔄 Prüfen...", "checking": "🔄 Prüfen...",
"check_now": "🔍 Jetzt nach Updates suchen" "check_now": "🔍 Jetzt nach Updates suchen",
"current_version": "Aktuelle yt-dlp-Version: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL kann nicht leer sein", "empty_url": "URL kann nicht leer sein",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Sind Sie sicher? Dies kann nicht rückgängig gemacht werden.", "clear_confirm_message": "Sind Sie sicher? Dies kann nicht rückgängig gemacht werden.",
"no_history": "Noch kein Verlauf", "no_history": "Noch kein Verlauf",
"no_history_description": "Ihre Downloads werden hier angezeigt", "no_history_description": "Ihre Downloads werden hier angezeigt",
"loading": "Verlauf wird geladen...",
"search_placeholder": "Suchen...", "search_placeholder": "Suchen...",
"open_location": "Speicherort Öffnen", "open_location": "Speicherort Öffnen",
"redownload": "Erneut Laden", "redownload": "Erneut Laden",
@@ -491,7 +527,47 @@
"one_entry": "1 Download", "one_entry": "1 Download",
"redownload_confirm_title": "Erneut Laden?", "redownload_confirm_title": "Erneut Laden?",
"redownload_confirm_message": "Erneut herunterladen?\n\n{title}", "redownload_confirm_message": "Erneut herunterladen?\n\n{title}",
"redownload_started": "Download gestartet" "redownload_started": "Download gestartet",
"no_url_error": "Keine URL im Verlaufseintrag gefunden",
"redownload_failed": "Neuer Download konnte nicht gestartet werden: {error}"
},
"ffmpeg": {
"installation_title": "FFmpeg-Installation",
"installation_message": "YTSage benötigt FFmpeg zur Verarbeitung von Videos.\n\nWähle unten eine Installationsoption:",
"install_button": "FFmpeg installieren",
"manual_guide": "Manuelle Anleitung",
"installation_failed": "Bei der FFmpeg-Installation ist ein Problem aufgetreten.",
"already_installed": "FFmpeg ist bereits installiert!",
"installation_complete": "Installation abgeschlossen. Sie können diesen Dialog schließen und YTSage weiter nutzen.",
"installing": "FFmpeg wird installiert... Bitte warten",
"install_success": "FFmpeg wurde erfolgreich installiert!",
"installation_complete_close": "Installation abgeschlossen. Sie können diesen Dialog jetzt schließen und YTSage weiter nutzen.",
"try_manual": "Bitte versuchen Sie stattdessen die manuelle Installationsanleitung."
},
"ytdlp_setup": {
"required_title": "yt-dlp-Einrichtung erforderlich",
"description": "YTSage benötigt yt-dlp zum Herunterladen von Videos.<br><br>yt-dlp wurde im lokalen App-Verzeichnis nicht gefunden. YTSage muss yt-dlp für dein {os_name}-System einrichten.<br><br>Bitte wähle unten eine Option:",
"option_auto": "Automatisch herunterladen (empfohlen)",
"option_manual": "Pfad manuell auswählen",
"setup_button": "yt-dlp einrichten",
"downloading": "yt-dlp wird heruntergeladen...",
"success": "yt-dlp wurde erfolgreich installiert!",
"error": "Fehler: {error}",
"download_failed_title": "Download fehlgeschlagen",
"download_failed_message": "yt-dlp konnte nicht heruntergeladen werden: {error}",
"select_executable_title": "yt-dlp-Programm auswählen",
"copied_to": "yt-dlp erfolgreich nach {path} kopiert",
"setup_error_title": "Einrichtungsfehler",
"copy_error": "Fehler beim Kopieren von yt-dlp ins App-Verzeichnis: {error}",
"invalid_executable_title": "Ungültige Datei",
"invalid_executable_message": "Die ausgewählte Datei scheint kein gültiges yt-dlp-Programm zu sein.",
"verify_error": "Fehler beim Prüfen der yt-dlp-Datei: {error}",
"setup_failed_title": "Einrichtung fehlgeschlagen",
"setup_failed_message": "yt-dlp konnte nicht eingerichtet werden. Einige Funktionen funktionieren möglicherweise nicht korrekt.",
"success_dialog_title": "yt-dlp-Einrichtung",
"success_dialog_message": "yt-dlp wurde erfolgreich eingerichtet unter:\n{path}",
"file_filter_windows": "Ausführbare Dateien (*.exe)",
"file_filter_all": "Alle Dateien (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "FFmpeg-Versionsprüfer", "title": "FFmpeg-Versionsprüfer",
+99 -22
View File
@@ -82,7 +82,8 @@
"select_defaults": "Select Defaults", "select_defaults": "Select Defaults",
"select_all": "Select All", "select_all": "Select All",
"deselect_all": "Deselect All", "deselect_all": "Deselect All",
"open_folder": "Open folder location" "open_folder": "Open folder location",
"reset": "Reset"
}, },
"dialogs": { "dialogs": {
"custom_options": "Custom Options", "custom_options": "Custom Options",
@@ -92,8 +93,10 @@
"sponsorblock_description": "Select which types of video segments to automatically remove during download.\nSponsorBlock uses community-submitted data to identify these segments.", "sponsorblock_description": "Select which types of video segments to automatically remove during download.\nSponsorBlock uses community-submitted data to identify these segments.",
"select_subtitles": "Select Subtitles", "select_subtitles": "Select Subtitles",
"filter_languages_placeholder": "Filter languages (e.g., en, es)...", "filter_languages_placeholder": "Filter languages (e.g., en, es)...",
"filter_playlist_placeholder": "Filter videos...",
"no_subtitles_available": "No subtitles available", "no_subtitles_available": "No subtitles available",
"matching": "matching" "matching": "matching",
"ytdlp_log_title": "yt-dlp Log"
}, },
"tabs": { "tabs": {
"cookies": "Login with Cookies", "cookies": "Login with Cookies",
@@ -124,7 +127,10 @@
"browser_selected_title": "Browser Cookies Applied", "browser_selected_title": "Browser Cookies Applied",
"browser_applied_message": "Browser cookies will be extracted from: {browser}", "browser_applied_message": "Browser cookies will be extracted from: {browser}",
"cleared_title": "Cookies Cleared", "cleared_title": "Cookies Cleared",
"cleared_message": "Cookie settings have been cleared" "cleared_message": "Cookie settings have been cleared",
"active_browser": "✓ Active: Browser cookies ({browser})",
"active_file": "✓ Active: Cookie file ({file})",
"none_active": "○ No cookies active"
}, },
"custom_command": { "custom_command": {
"help_text": "Enter your custom yt-dlp command below. The current URL will be appended automatically.<br><br>For the full list of options and usage examples, <a href=\"{docs_url}\">click here to view the official yt-dlp documentation</a>.<br><br>Note: Download path and filename template will be handled automatically.", "help_text": "Enter your custom yt-dlp command below. The current URL will be appended automatically.<br><br>For the full list of options and usage examples, <a href=\"{docs_url}\">click here to view the official yt-dlp documentation</a>.<br><br>Note: Download path and filename template will be handled automatically.",
@@ -137,7 +143,14 @@
"full_command": "🔧 Full command: {command}", "full_command": "🔧 Full command: {command}",
"command_success": "✅ Custom command completed successfully!", "command_success": "✅ Custom command completed successfully!",
"command_failed": "❌ Command failed with exit code {code}", "command_failed": "❌ Command failed with exit code {code}",
"command_error": "❌ Error running custom command: {error}" "command_error": "❌ Error running custom command: {error}",
"error_no_url": "❌ Error: No URL provided. Please enter a URL in the main window.",
"error_no_command": "❌ Error: No command provided. Please enter yt-dlp arguments.",
"executing": "🚀 Executing custom yt-dlp command",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Arguments: {command}",
"download_path_label": "📁 Download path: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Configure proxy settings for downloading. Leave empty to use direct connection.", "help_text": "Configure proxy settings for downloading. Leave empty to use direct connection.",
@@ -159,7 +172,15 @@
"invalid_main_url": "Invalid main proxy URL format", "invalid_main_url": "Invalid main proxy URL format",
"invalid_geo_url": "Invalid geo proxy URL format", "invalid_geo_url": "Invalid geo proxy URL format",
"main_configured": "Main proxy configured", "main_configured": "Main proxy configured",
"geo_configured": "Geo proxy configured" "geo_configured": "Geo proxy configured",
"set_title": "Proxy Set",
"set_message": "Main proxy set and saved: {proxy}",
"geo_set_title": "Geo Proxy Set",
"geo_set_message": "Geo-verification proxy set and saved: {proxy}",
"cleared_title": "Proxy Settings Cleared",
"cleared_message": "All proxy settings have been cleared and saved.",
"saved_main": "Saved main proxy: {proxy}",
"saved_geo": "Saved geo proxy: {proxy}"
}, },
"download": { "download": {
"preparing": "Preparing download...", "preparing": "Preparing download...",
@@ -225,6 +246,8 @@
"system_info": "System Information", "system_info": "System Information",
"loading": "🔄 Loading system information...", "loading": "🔄 Loading system information...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Logs",
"logs_tooltip": "Open application logs folder",
"refreshing": "🔄 Refreshing...", "refreshing": "🔄 Refreshing...",
"refresh_failed": "Refresh Failed", "refresh_failed": "Refresh Failed",
"refresh_failed_message": "Could not refresh version information.", "refresh_failed_message": "Could not refresh version information.",
@@ -277,6 +300,8 @@
"ytdlp_channel_switched": "✅ Successfully switched to {channel} channel!", "ytdlp_channel_switched": "✅ Successfully switched to {channel} channel!",
"ytdlp_channel_switch_failed": "❌ Failed to switch channel: {error}", "ytdlp_channel_switch_failed": "❌ Failed to switch channel: {error}",
"ytdlp_current_channel": "Current channel: {channel}", "ytdlp_current_channel": "Current channel: {channel}",
"app_updates_title": "YTSage Updates",
"check_beta_updates": "Receive Beta Updates",
"auto_update_title": "Auto-Update Settings", "auto_update_title": "Auto-Update Settings",
"auto_update_header": "🔄 Auto-Update Settings", "auto_update_header": "🔄 Auto-Update Settings",
"auto_update_description": "Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.", "auto_update_description": "Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.",
@@ -298,6 +323,8 @@
"format_mkv": "MKV (Feature-rich)", "format_mkv": "MKV (Feature-rich)",
"audio_format_settings": "Audio Format Settings", "audio_format_settings": "Audio Format Settings",
"force_audio_format": "Force audio format for audio-only downloads", "force_audio_format": "Force audio format for audio-only downloads",
"filename_format": "Output Filename Format",
"filename_format_help": "Available variables: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Standard yt-dlp output template syntax is supported.",
"preferred_audio_format": "Preferred audio format:", "preferred_audio_format": "Preferred audio format:",
"force_audio_format_help": "When enabled, audio-only downloads will be converted to your preferred format. This only applies when downloading audio formats.", "force_audio_format_help": "When enabled, audio-only downloads will be converted to your preferred format. This only applies when downloading audio formats.",
"audio_format_best": "Best (No conversion)", "audio_format_best": "Best (No conversion)",
@@ -326,27 +353,32 @@
"browser_cookies_selected_message": "Browser cookies will be extracted from: {browser}", "browser_cookies_selected_message": "Browser cookies will be extracted from: {browser}",
"error_no_format_info": "Error: No format information available.", "error_no_format_info": "Error: No format information available.",
"error_extract_info": "Error: Could not extract basic video information. Please check your link.", "error_extract_info": "Error: Could not extract basic video information. Please check your link.",
"analyzing_preparing": "Analyzing (0%)... Preparing request", "analyzing_preparing": "Preparing request...",
"analyzing_extracting_basic": "Analyzing (15%)... Extracting basic info", "analyzing_extracting_basic": "Extracting basic info...",
"analyzing_extracting_detailed": "Analyzing (30%)... Extracting detailed info", "analyzing_extracting_detailed": "Extracting detailed info...",
"analyzing_processing_video": "Analyzing (45%)... Processing video data", "analyzing_processing_video": "Processing video data...",
"analyzing_processing_formats": "Analyzing (60%)... Processing formats", "analyzing_processing_formats": "Processing formats...",
"analyzing_loading_thumbnail": "Analyzing (75%)... Loading thumbnail", "analyzing_loading_thumbnail": "Loading thumbnail...",
"analyzing_processing_subtitles": "Analyzing (85%)... Processing subtitles", "analyzing_processing_subtitles": "Processing subtitles...",
"analyzing_updating_table": "Analyzing (95%)... Updating format table", "analyzing_updating_table": "Updating format table...",
"analysis_complete": "Analysis complete!", "analysis_complete": "Analysis complete!",
"analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info", "analyzing_extracting_ytdlp": "Extracting info...",
"analyzing_processing_data": "Analyzing (60%)... Processing data", "analyzing_fetching_first_video": "Fetching formats for first video...",
"analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats", "analyzing_processing_data": "Processing data...",
"analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail", "analyzing_processing_formats_ytdlp": "Processing formats...",
"analyzing_processing_subtitles_ytdlp": "Analyzing (90%)... Processing subtitles", "analyzing_loading_thumbnail_ytdlp": "Loading thumbnail...",
"analyzing_processing_subtitles_ytdlp": "Processing subtitles...",
"select_subtitles": "Select Subtitles...", "select_subtitles": "Select Subtitles...",
"sponsorblock_categories": "SponsorBlock Categories...", "sponsorblock_categories": "SponsorBlock Categories...",
"invalid_url_or_enter": "Invalid URL or please enter a URL.", "invalid_url_or_enter": "Invalid URL or please enter a URL.",
"zero_selected": "0 selected", "zero_selected": "0 selected",
"analyze_first_tooltip": "Please analyze the video first", "analyze_first_tooltip": "Please analyze the video first",
"audio_mode_disabled": "Not available in audio-only mode", "audio_mode_disabled": "Not available in audio-only mode",
"select_subtitles_first": "Please select subtitles first" "select_subtitles_first": "Please select subtitles first",
"settings_tooltip": "Current Path: {path}\nSpeed Limit: {speed_limit}",
"speed_limit_none": "None",
"open_folder_error": "Could not open folder: {error}",
"time_range_set": "Section set: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +440,10 @@
"ytdlp_failed": "Error: yt-dlp failed: {error}", "ytdlp_failed": "Error: yt-dlp failed: {error}",
"parse_failed": "Error: Failed to parse yt-dlp output: {error}", "parse_failed": "Error: Failed to parse yt-dlp output: {error}",
"analysis_failed": "Error: Analysis failed: {error}", "analysis_failed": "Error: Analysis failed: {error}",
"generic_error": "Error: {error}" "generic_error": "Error: {error}",
"download_failed_return_code_conflict": "Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application.",
"download_failed_return_code": "Download failed with return code {return_code}",
"direct_command_error": "Error in direct command: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Update Available", "title": "Update Available",
@@ -442,7 +477,8 @@
"next_check": "Next check: {time}", "next_check": "Next check: {time}",
"next_check_error": "Next check: Error calculating", "next_check_error": "Next check: Error calculating",
"checking": "🔄 Checking...", "checking": "🔄 Checking...",
"check_now": "🔍 Check for Updates Now" "check_now": "🔍 Check for Updates Now",
"current_version": "Current yt-dlp version: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL cannot be empty", "empty_url": "URL cannot be empty",
@@ -473,6 +509,7 @@
"clear_confirm_message": "Are you sure you want to clear all download history? This cannot be undone.", "clear_confirm_message": "Are you sure you want to clear all download history? This cannot be undone.",
"no_history": "No download history yet", "no_history": "No download history yet",
"no_history_description": "Your downloaded videos and audio will appear here", "no_history_description": "Your downloaded videos and audio will appear here",
"loading": "Loading history...",
"search_placeholder": "Search history...", "search_placeholder": "Search history...",
"open_location": "Open File Location", "open_location": "Open File Location",
"redownload": "Redownload", "redownload": "Redownload",
@@ -491,7 +528,47 @@
"one_entry": "1 download", "one_entry": "1 download",
"redownload_confirm_title": "Redownload Video?", "redownload_confirm_title": "Redownload Video?",
"redownload_confirm_message": "Download this video again using the same settings?\n\n{title}", "redownload_confirm_message": "Download this video again using the same settings?\n\n{title}",
"redownload_started": "Redownload started" "redownload_started": "Redownload started",
"no_url_error": "No URL found in history entry",
"redownload_failed": "Failed to start redownload: {error}"
},
"ffmpeg": {
"installation_title": "FFmpeg Installation",
"installation_message": "YTSage needs FFmpeg to process videos.\n\nChoose an installation option below:",
"install_button": "Install FFmpeg",
"manual_guide": "Manual Guide",
"installation_failed": "FFmpeg installation encountered an issue.",
"already_installed": "FFmpeg is already installed!",
"installation_complete": "Installation complete. You can close this dialog and continue using YTSage.",
"installing": "Installing FFmpeg... Please wait",
"install_success": "FFmpeg has been installed successfully!",
"installation_complete_close": "Installation complete. You can now close this dialog and continue using YTSage.",
"try_manual": "Please try using the manual installation guide instead."
},
"ytdlp_setup": {
"required_title": "yt-dlp Setup Required",
"description": "YTSage requires yt-dlp to download videos.<br><br>yt-dlp was not found in the app's local directory. YTSage needs to set up yt-dlp for your {os_name} system.<br><br>Please choose an option below:",
"option_auto": "Download automatically (Recommended)",
"option_manual": "Select path manually",
"setup_button": "Setup yt-dlp",
"downloading": "Downloading yt-dlp...",
"success": "yt-dlp was successfully installed!",
"error": "Error: {error}",
"download_failed_title": "Download Failed",
"download_failed_message": "Failed to download yt-dlp: {error}",
"select_executable_title": "Select yt-dlp executable",
"copied_to": "yt-dlp successfully copied to {path}",
"setup_error_title": "Setup Error",
"copy_error": "Error copying yt-dlp to app directory: {error}",
"invalid_executable_title": "Invalid Executable",
"invalid_executable_message": "The selected file does not appear to be a valid yt-dlp executable.",
"verify_error": "Error verifying yt-dlp executable: {error}",
"setup_failed_title": "Setup Failed",
"setup_failed_message": "Failed to set up yt-dlp. Some features may not work correctly.",
"success_dialog_title": "yt-dlp Setup",
"success_dialog_message": "yt-dlp has been successfully configured at:\n{path}",
"file_filter_windows": "Executable Files (*.exe)",
"file_filter_all": "All Files (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "FFmpeg Version Checker", "title": "FFmpeg Version Checker",
+98 -22
View File
@@ -25,6 +25,7 @@
"ready": "Listo" "ready": "Listo"
}, },
"buttons": { "buttons": {
"reset": "Restablecer",
"download": "Descargar", "download": "Descargar",
"pause": "Pausar", "pause": "Pausar",
"resume": "Reanudar", "resume": "Reanudar",
@@ -60,7 +61,8 @@
"select_subtitles": "Seleccionar Subtítulos", "select_subtitles": "Seleccionar Subtítulos",
"filter_languages_placeholder": "Filtrar idiomas (ej., en, es)...", "filter_languages_placeholder": "Filtrar idiomas (ej., en, es)...",
"no_subtitles_available": "No hay subtítulos disponibles", "no_subtitles_available": "No hay subtítulos disponibles",
"matching": "que coincidan con" "matching": "que coincidan con",
"ytdlp_log_title": "Registro de yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Iniciar sesión con Cookies", "cookies": "Iniciar sesión con Cookies",
@@ -91,7 +93,10 @@
"browser_selected_title": "Cookies del Navegador Aplicadas", "browser_selected_title": "Cookies del Navegador Aplicadas",
"browser_applied_message": "Las cookies del navegador se extraerán de: {browser}", "browser_applied_message": "Las cookies del navegador se extraerán de: {browser}",
"cleared_title": "Cookies Borradas", "cleared_title": "Cookies Borradas",
"cleared_message": "La configuración de cookies se ha borrado" "cleared_message": "La configuración de cookies se ha borrado",
"active_browser": "✓ Activas: cookies del navegador ({browser})",
"active_file": "✓ Activo: archivo de cookies ({file})",
"none_active": "○ No hay cookies activas"
}, },
"custom_command": { "custom_command": {
"help_text": "Ingresa tu comando yt-dlp personalizado a continuación. La URL actual se añadirá automáticamente.<br><br>Para ver la lista completa de opciones y ejemplos de uso, <a href=\"{docs_url}\">haz clic aquí para ver la documentación oficial de yt-dlp</a>.<br><br>Nota: La ruta de descarga y la plantilla de nombre de archivo se manejarán automáticamente.", "help_text": "Ingresa tu comando yt-dlp personalizado a continuación. La URL actual se añadirá automáticamente.<br><br>Para ver la lista completa de opciones y ejemplos de uso, <a href=\"{docs_url}\">haz clic aquí para ver la documentación oficial de yt-dlp</a>.<br><br>Nota: La ruta de descarga y la plantilla de nombre de archivo se manejarán automáticamente.",
@@ -102,7 +107,14 @@
"full_command": "🔧 Comando completo: {command}", "full_command": "🔧 Comando completo: {command}",
"command_failed": "❌ El comando falló con código de salida {code}", "command_failed": "❌ El comando falló con código de salida {code}",
"command_success": "✅ ¡Comando completado exitosamente!", "command_success": "✅ ¡Comando completado exitosamente!",
"command_error": "❌ Error ejecutando comando: {error}" "command_error": "❌ Error ejecutando comando: {error}",
"error_no_url": "❌ Error: No se proporcionó URL. Ingresa una URL en la ventana principal.",
"error_no_command": "❌ Error: No se proporcionó comando. Ingresa argumentos de yt-dlp.",
"executing": "🚀 Ejecutando comando personalizado de yt-dlp",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argumentos: {command}",
"download_path_label": "📁 Ruta de descarga: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Configurar ajustes de proxy para conexiones de red y geo-verificación.\nEl proxy puede ayudar a evitar restricciones regionales y mejorar el rendimiento de descarga.", "help_text": "Configurar ajustes de proxy para conexiones de red y geo-verificación.\nEl proxy puede ayudar a evitar restricciones regionales y mejorar el rendimiento de descarga.",
@@ -120,7 +132,15 @@
"invalid_main_url": "Formato de URL de proxy principal inválido", "invalid_main_url": "Formato de URL de proxy principal inválido",
"invalid_geo_url": "Formato de URL de geo proxy inválido", "invalid_geo_url": "Formato de URL de geo proxy inválido",
"main_configured": "Proxy principal configurado", "main_configured": "Proxy principal configurado",
"geo_configured": "Geo proxy configurado" "geo_configured": "Geo proxy configurado",
"set_title": "Proxy establecido",
"set_message": "Proxy principal establecido y guardado: {proxy}",
"geo_set_title": "Proxy geo establecido",
"geo_set_message": "Proxy de verificación geográfica establecido y guardado: {proxy}",
"cleared_title": "Configuración de proxy borrada",
"cleared_message": "Se borró y guardó toda la configuración de proxy.",
"saved_main": "Proxy principal guardado: {proxy}",
"saved_geo": "Proxy geo guardado: {proxy}"
}, },
"download": { "download": {
"preparing": "Preparando descarga...", "preparing": "Preparando descarga...",
@@ -185,6 +205,8 @@
"system_info": "Información del Sistema", "system_info": "Información del Sistema",
"loading": "🔄 Cargando información del sistema...", "loading": "🔄 Cargando información del sistema...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Registros",
"logs_tooltip": "Abrir carpeta de registros de la aplicación",
"refreshing": "🔄 Actualizando...", "refreshing": "🔄 Actualizando...",
"refresh_failed": "Actualización Fallida", "refresh_failed": "Actualización Fallida",
"refresh_failed_message": "No se pudo actualizar la información de versión.", "refresh_failed_message": "No se pudo actualizar la información de versión.",
@@ -260,6 +282,8 @@
"ytdlp_channel_switched": "✅ ¡Cambiado exitosamente al canal {channel}!", "ytdlp_channel_switched": "✅ ¡Cambiado exitosamente al canal {channel}!",
"ytdlp_channel_switch_failed": "❌ Error al cambiar de canal: {error}", "ytdlp_channel_switch_failed": "❌ Error al cambiar de canal: {error}",
"ytdlp_current_channel": "Canal actual: {channel}", "ytdlp_current_channel": "Canal actual: {channel}",
"app_updates_title": "Actualizaciones de YTSage",
"check_beta_updates": "Recibir actualizaciones beta",
"auto_update_title": "Configuración de Auto-Actualización", "auto_update_title": "Configuración de Auto-Actualización",
"auto_update_header": "🔄 Configuración de Auto-Actualización", "auto_update_header": "🔄 Configuración de Auto-Actualización",
"auto_update_description": "Configura las actualizaciones automáticas de yt-dlp para asegurar que siempre tengas las últimas funciones y correcciones de errores.", "auto_update_description": "Configura las actualizaciones automáticas de yt-dlp para asegurar que siempre tengas las últimas funciones y correcciones de errores.",
@@ -290,7 +314,9 @@
"audio_format_wav": "WAV (Sin comprimir)", "audio_format_wav": "WAV (Sin comprimir)",
"audio_format_opus": "Opus (Eficiente)", "audio_format_opus": "Opus (Eficiente)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Abierto)" "audio_format_vorbis": "Vorbis (Abierto)",
"filename_format": "Formato de nombre de archivo",
"filename_format_help": "Variables disponibles: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Se admite la sintaxis estándar de plantilla de salida de yt-dlp."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Ingresa URL de video o lista de YouTube", "url_placeholder": "Ingresa URL de video o lista de YouTube",
@@ -309,27 +335,32 @@
"browser_cookies_selected_message": "Las cookies del navegador serán extraídas de: {browser}", "browser_cookies_selected_message": "Las cookies del navegador serán extraídas de: {browser}",
"error_no_format_info": "Error: No hay información de formato disponible.", "error_no_format_info": "Error: No hay información de formato disponible.",
"error_extract_info": "Error: No se pudo extraer la información básica del video. Por favor verifica tu enlace.", "error_extract_info": "Error: No se pudo extraer la información básica del video. Por favor verifica tu enlace.",
"analyzing_preparing": "Analizando (0%)... Preparando solicitud", "analyzing_preparing": "Preparando solicitud...",
"analyzing_extracting_basic": "Analizando (15%)... Extrayendo información básica", "analyzing_extracting_basic": "Extrayendo información básica...",
"analyzing_extracting_detailed": "Analizando (30%)... Extrayendo información detallada", "analyzing_extracting_detailed": "Extrayendo información detallada...",
"analyzing_processing_video": "Analizando (45%)... Procesando datos de video", "analyzing_processing_video": "Procesando datos de video...",
"analyzing_processing_formats": "Analizando (60%)... Procesando formatos", "analyzing_processing_formats": "Procesando formatos...",
"analyzing_loading_thumbnail": "Analizando (75%)... Cargando miniatura", "analyzing_loading_thumbnail": "Cargando miniatura...",
"analyzing_processing_subtitles": "Analizando (85%)... Procesando subtítulos", "analyzing_processing_subtitles": "Procesando subtítulos...",
"analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos", "analyzing_updating_table": "Actualizando tabla de formatos...",
"analysis_complete": "¡Análisis completo!", "analysis_complete": "¡Análisis completo!",
"analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo información", "analyzing_extracting_ytdlp": "Extrayendo información...",
"analyzing_processing_data": "Analizando (60%)... Procesando datos", "analyzing_fetching_first_video": "Obteniendo formatos del primer video...",
"analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos", "analyzing_processing_data": "Procesando datos...",
"analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura", "analyzing_processing_formats_ytdlp": "Procesando formatos...",
"analyzing_processing_subtitles_ytdlp": "Analizando (90%)... Procesando subtítulos", "analyzing_loading_thumbnail_ytdlp": "Cargando miniatura...",
"analyzing_processing_subtitles_ytdlp": "Procesando subtítulos...",
"select_subtitles": "Seleccionar Subtítulos...", "select_subtitles": "Seleccionar Subtítulos...",
"sponsorblock_categories": "Categorías SponsorBlock...", "sponsorblock_categories": "Categorías SponsorBlock...",
"invalid_url_or_enter": "URL inválida o por favor ingresa una URL.", "invalid_url_or_enter": "URL inválida o por favor ingresa una URL.",
"zero_selected": "0 seleccionados", "zero_selected": "0 seleccionados",
"analyze_first_tooltip": "Por favor analiza el video primero", "analyze_first_tooltip": "Por favor analiza el video primero",
"audio_mode_disabled": "No disponible en modo solo audio", "audio_mode_disabled": "No disponible en modo solo audio",
"select_subtitles_first": "Por favor selecciona subtítulos primero" "select_subtitles_first": "Por favor selecciona subtítulos primero",
"settings_tooltip": "Ruta actual: {path}\nLímite de velocidad: {speed_limit}",
"speed_limit_none": "Ninguno",
"open_folder_error": "No se pudo abrir la carpeta: {error}",
"time_range_set": "Sección establecida: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Patrocinador", "sponsor": "Patrocinador",
@@ -391,7 +422,10 @@
"ytdlp_failed": "Error: yt-dlp falló: {error}", "ytdlp_failed": "Error: yt-dlp falló: {error}",
"parse_failed": "Error: Falló al analizar salida de yt-dlp: {error}", "parse_failed": "Error: Falló al analizar salida de yt-dlp: {error}",
"analysis_failed": "Error: Análisis falló: {error}", "analysis_failed": "Error: Análisis falló: {error}",
"generic_error": "Error: {error}" "generic_error": "Error: {error}",
"download_failed_return_code_conflict": "La descarga falló con el código de salida {return_code}. Esto puede deberse a un conflicto con varias instalaciones de yt-dlp. Intenta desinstalar cualquier yt-dlp instalado en el sistema (p. ej., mediante snap o apt) y reinicia la aplicación.",
"download_failed_return_code": "La descarga falló con el código de salida {return_code}",
"direct_command_error": "Error en el comando directo: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Actualización disponible", "title": "Actualización disponible",
@@ -425,7 +459,8 @@
"next_check": "Próxima verificación: {time}", "next_check": "Próxima verificación: {time}",
"next_check_error": "Próxima verificación: Error al calcular", "next_check_error": "Próxima verificación: Error al calcular",
"checking": "🔄 Verificando...", "checking": "🔄 Verificando...",
"check_now": "🔍 Verificar actualizaciones ahora" "check_now": "🔍 Verificar actualizaciones ahora",
"current_version": "Versión actual de yt-dlp: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "La URL no puede estar vacía", "empty_url": "La URL no puede estar vacía",
@@ -456,6 +491,7 @@
"clear_confirm_message": "¿Estás seguro de que quieres borrar todo el historial? Esto no se puede deshacer.", "clear_confirm_message": "¿Estás seguro de que quieres borrar todo el historial? Esto no se puede deshacer.",
"no_history": "Aún no hay historial", "no_history": "Aún no hay historial",
"no_history_description": "Tus descargas aparecerán aquí", "no_history_description": "Tus descargas aparecerán aquí",
"loading": "Cargando historial...",
"search_placeholder": "Buscar...", "search_placeholder": "Buscar...",
"open_location": "Abrir Ubicación", "open_location": "Abrir Ubicación",
"redownload": "Descargar de Nuevo", "redownload": "Descargar de Nuevo",
@@ -474,7 +510,47 @@
"one_entry": "1 descarga", "one_entry": "1 descarga",
"redownload_confirm_title": "¿Descargar de Nuevo?", "redownload_confirm_title": "¿Descargar de Nuevo?",
"redownload_confirm_message": "¿Descargar nuevamente?\n\n{title}", "redownload_confirm_message": "¿Descargar nuevamente?\n\n{title}",
"redownload_started": "Descarga iniciada" "redownload_started": "Descarga iniciada",
"no_url_error": "No se encontró URL en la entrada del historial",
"redownload_failed": "No se pudo iniciar la re-descarga: {error}"
},
"ffmpeg": {
"installation_title": "Instalación de FFmpeg",
"installation_message": "YTSage necesita FFmpeg para procesar videos.\n\nElige una opción de instalación:",
"install_button": "Instalar FFmpeg",
"manual_guide": "Guía manual",
"installation_failed": "La instalación de FFmpeg encontró un problema.",
"already_installed": "¡FFmpeg ya está instalado!",
"installation_complete": "Instalación completa. Puedes cerrar este diálogo y seguir usando YTSage.",
"installing": "Instalando FFmpeg... Por favor espera",
"install_success": "¡FFmpeg se instaló correctamente!",
"installation_complete_close": "Instalación completa. Ahora puedes cerrar este diálogo y seguir usando YTSage.",
"try_manual": "Intenta usar la guía de instalación manual en su lugar."
},
"ytdlp_setup": {
"required_title": "Se requiere configurar yt-dlp",
"description": "YTSage requiere yt-dlp para descargar videos.<br><br>No se encontró yt-dlp en el directorio local de la app. YTSage necesita configurar yt-dlp para tu sistema {os_name}.<br><br>Elige una opción:",
"option_auto": "Descargar automáticamente (recomendado)",
"option_manual": "Seleccionar ruta manualmente",
"setup_button": "Configurar yt-dlp",
"downloading": "Descargando yt-dlp...",
"success": "¡yt-dlp se instaló correctamente!",
"error": "Error: {error}",
"download_failed_title": "Descarga fallida",
"download_failed_message": "No se pudo descargar yt-dlp: {error}",
"select_executable_title": "Seleccionar ejecutable de yt-dlp",
"copied_to": "yt-dlp se copió correctamente a {path}",
"setup_error_title": "Error de configuración",
"copy_error": "Error al copiar yt-dlp al directorio de la app: {error}",
"invalid_executable_title": "Ejecutable inválido",
"invalid_executable_message": "El archivo seleccionado no parece ser un ejecutable válido de yt-dlp.",
"verify_error": "Error al verificar el ejecutable de yt-dlp: {error}",
"setup_failed_title": "Configuración fallida",
"setup_failed_message": "No se pudo configurar yt-dlp. Algunas funciones pueden no funcionar correctamente.",
"success_dialog_title": "Configuración de yt-dlp",
"success_dialog_message": "yt-dlp se configuró correctamente en:\n{path}",
"file_filter_windows": "Archivos ejecutables (*.exe)",
"file_filter_all": "Todos los archivos (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Comprobador de Versión FFmpeg", "title": "Comprobador de Versión FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Audio uniquement" "audio_only_resolution": "Audio uniquement"
}, },
"buttons": { "buttons": {
"reset": "Réinitialiser",
"download": "Télécharger", "download": "Télécharger",
"pause": "Pause", "pause": "Pause",
"resume": "Reprendre", "resume": "Reprendre",
@@ -93,7 +94,8 @@
"select_subtitles": "Sélectionner les sous-titres", "select_subtitles": "Sélectionner les sous-titres",
"filter_languages_placeholder": "Filtrer les langues (ex: en, fr)...", "filter_languages_placeholder": "Filtrer les langues (ex: en, fr)...",
"no_subtitles_available": "Aucun sous-titre disponible", "no_subtitles_available": "Aucun sous-titre disponible",
"matching": "correspondant" "matching": "correspondant",
"ytdlp_log_title": "Journal yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Se connecter avec des cookies", "cookies": "Se connecter avec des cookies",
@@ -124,7 +126,10 @@
"browser_selected_title": "Cookies du navigateur appliqués", "browser_selected_title": "Cookies du navigateur appliqués",
"browser_applied_message": "Les cookies du navigateur seront extraits de : {browser}", "browser_applied_message": "Les cookies du navigateur seront extraits de : {browser}",
"cleared_title": "Cookies effacés", "cleared_title": "Cookies effacés",
"cleared_message": "Les paramètres de cookies ont été effacés" "cleared_message": "Les paramètres des cookies ont été effacés",
"active_browser": "✓ Actifs : cookies du navigateur ({browser})",
"active_file": "✓ Actif : fichier cookies ({file})",
"none_active": "○ Aucun cookie actif"
}, },
"custom_command": { "custom_command": {
"help_text": "Entrez votre commande yt-dlp personnalisée ci-dessous. L'URL actuelle sera automatiquement ajoutée.<br><br>Pour la liste complète des options et exemples d'utilisation <a href=\"{docs_url}\">cliquez ici pour voir la documentation officielle yt-dlp</a>.<br><br>Note : Le chemin de téléchargement et le modèle de nom de fichier sont gérés automatiquement.", "help_text": "Entrez votre commande yt-dlp personnalisée ci-dessous. L'URL actuelle sera automatiquement ajoutée.<br><br>Pour la liste complète des options et exemples d'utilisation <a href=\"{docs_url}\">cliquez ici pour voir la documentation officielle yt-dlp</a>.<br><br>Note : Le chemin de téléchargement et le modèle de nom de fichier sont gérés automatiquement.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Commande complète : {command}", "full_command": "🔧 Commande complète : {command}",
"command_success": "✅ Commande personnalisée exécutée avec succès !", "command_success": "✅ Commande personnalisée exécutée avec succès !",
"command_failed": "❌ Commande échouée avec le code de sortie {code}", "command_failed": "❌ Commande échouée avec le code de sortie {code}",
"command_error": "❌ Erreur lors de l'exécution de la commande personnalisée : {error}" "command_error": "❌ Erreur lors de l'exécution de la commande personnalisée : {error}",
"error_no_url": "❌ Erreur : aucune URL fournie. Veuillez saisir une URL dans la fenêtre principale.",
"error_no_command": "❌ Erreur : aucune commande fournie. Veuillez saisir des arguments yt-dlp.",
"executing": "🚀 Exécution de la commande yt-dlp personnalisée",
"url_label": "📍 URL : {url}",
"args_label": "⚙️ Arguments : {command}",
"download_path_label": "📁 Chemin de téléchargement : {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Configurer les paramètres proxy pour les téléchargements. Laisser vide pour une connexion directe.", "help_text": "Configurer les paramètres proxy pour les téléchargements. Laisser vide pour une connexion directe.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Format d'URL de proxy principal invalide", "invalid_main_url": "Format d'URL de proxy principal invalide",
"invalid_geo_url": "Format d'URL de proxy géographique invalide", "invalid_geo_url": "Format d'URL de proxy géographique invalide",
"main_configured": "Proxy principal configuré", "main_configured": "Proxy principal configuré",
"geo_configured": "Proxy géographique configuré" "geo_configured": "Proxy géographique configuré",
"set_title": "Proxy défini",
"set_message": "Proxy principal défini et enregistré : {proxy}",
"geo_set_title": "Proxy géo défini",
"geo_set_message": "Proxy de vérification géographique défini et enregistré : {proxy}",
"cleared_title": "Paramètres proxy effacés",
"cleared_message": "Tous les paramètres proxy ont été effacés et enregistrés.",
"saved_main": "Proxy principal enregistré : {proxy}",
"saved_geo": "Proxy géo enregistré : {proxy}"
}, },
"download": { "download": {
"preparing": "Préparation du téléchargement...", "preparing": "Préparation du téléchargement...",
@@ -225,6 +245,8 @@
"system_info": "Informations système", "system_info": "Informations système",
"loading": "🔄 Chargement des informations système...", "loading": "🔄 Chargement des informations système...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Journaux",
"logs_tooltip": "Ouvrir le dossier des journaux d'application",
"refreshing": "🔄 Actualisation...", "refreshing": "🔄 Actualisation...",
"refresh_failed": "Échec de l'actualisation", "refresh_failed": "Échec de l'actualisation",
"refresh_failed_message": "Impossible d'actualiser les informations de version.", "refresh_failed_message": "Impossible d'actualiser les informations de version.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Passage réussi au canal {channel} !", "ytdlp_channel_switched": "✅ Passage réussi au canal {channel} !",
"ytdlp_channel_switch_failed": "❌ Échec du changement de canal : {error}", "ytdlp_channel_switch_failed": "❌ Échec du changement de canal : {error}",
"ytdlp_current_channel": "Canal actuel : {channel}", "ytdlp_current_channel": "Canal actuel : {channel}",
"app_updates_title": "Mises à jour YTSage",
"check_beta_updates": "Recevoir les mises à jour bêta",
"auto_update_title": "Paramètres de mise à jour automatique", "auto_update_title": "Paramètres de mise à jour automatique",
"auto_update_header": "🔄 Paramètres de mise à jour automatique", "auto_update_header": "🔄 Paramètres de mise à jour automatique",
"auto_update_description": "Configurez les mises à jour automatiques pour yt-dlp afin de vous assurer d'avoir toujours les dernières fonctionnalités et corrections de bogues.", "auto_update_description": "Configurez les mises à jour automatiques pour yt-dlp afin de vous assurer d'avoir toujours les dernières fonctionnalités et corrections de bogues.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Non compressé)", "audio_format_wav": "WAV (Non compressé)",
"audio_format_opus": "Opus (Efficace)", "audio_format_opus": "Opus (Efficace)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Ouvert)" "audio_format_vorbis": "Vorbis (Ouvert)",
"filename_format": "Format du nom de fichier",
"filename_format_help": "Variables disponibles : %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. La syntaxe standard des modèles de sortie yt-dlp est prise en charge."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Entrer l'URL de la vidéo ou de la playlist YouTube", "url_placeholder": "Entrer l'URL de la vidéo ou de la playlist YouTube",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "Les cookies du navigateur seront extraits depuis : {browser}", "browser_cookies_selected_message": "Les cookies du navigateur seront extraits depuis : {browser}",
"error_no_format_info": "Erreur : Aucune information de format disponible.", "error_no_format_info": "Erreur : Aucune information de format disponible.",
"error_extract_info": "Erreur : Impossible d'extraire les informations de base de la vidéo. Veuillez vérifier votre lien.", "error_extract_info": "Erreur : Impossible d'extraire les informations de base de la vidéo. Veuillez vérifier votre lien.",
"analyzing_preparing": "Analyse (0%)... Préparation de la requête", "analyzing_preparing": "Préparation de la requête...",
"analyzing_extracting_basic": "Analyse (15%)... Extraction des informations de base", "analyzing_extracting_basic": "Extraction des informations de base...",
"analyzing_extracting_detailed": "Analyse (30%)... Extraction des informations détaillées", "analyzing_extracting_detailed": "Extraction des informations détaillées...",
"analyzing_processing_video": "Analyse (45%)... Traitement des données vidéo", "analyzing_processing_video": "Traitement des données vidéo...",
"analyzing_processing_formats": "Analyse (60%)... Traitement des formats", "analyzing_processing_formats": "Traitement des formats...",
"analyzing_loading_thumbnail": "Analyse (75%)... Chargement de la miniature", "analyzing_loading_thumbnail": "Chargement de la miniature...",
"analyzing_processing_subtitles": "Analyse (85%)... Traitement des sous-titres", "analyzing_processing_subtitles": "Traitement des sous-titres...",
"analyzing_updating_table": "Analyse (95%)... Mise à jour du tableau des formats", "analyzing_updating_table": "Mise à jour du tableau des formats...",
"analysis_complete": "Analyse terminée !", "analysis_complete": "Analyse terminée !",
"analyzing_extracting_ytdlp": "Analyse (30%)... Extraction d'informations", "analyzing_extracting_ytdlp": "Extraction d'informations...",
"analyzing_processing_data": "Analyse (60%)... Traitement des données", "analyzing_fetching_first_video": "Récupération des formats pour la première vidéo...",
"analyzing_processing_formats_ytdlp": "Analyse (75%)... Traitement des formats", "analyzing_processing_data": "Traitement des données...",
"analyzing_loading_thumbnail_ytdlp": "Analyse (85%)... Chargement de la miniature", "analyzing_processing_formats_ytdlp": "Traitement des formats...",
"analyzing_processing_subtitles_ytdlp": "Analyse (90%)... Traitement des sous-titres", "analyzing_loading_thumbnail_ytdlp": "Chargement de la miniature...",
"analyzing_processing_subtitles_ytdlp": "Traitement des sous-titres...",
"select_subtitles": "Sélectionner les sous-titres...", "select_subtitles": "Sélectionner les sous-titres...",
"sponsorblock_categories": "Catégories SponsorBlock...", "sponsorblock_categories": "Catégories SponsorBlock...",
"invalid_url_or_enter": "URL invalide ou veuillez entrer une URL.", "invalid_url_or_enter": "URL invalide ou veuillez entrer une URL.",
"zero_selected": "0 sélectionné", "zero_selected": "0 sélectionné",
"analyze_first_tooltip": "Veuillez d'abord analyser la vidéo", "analyze_first_tooltip": "Veuillez d'abord analyser la vidéo",
"audio_mode_disabled": "Non disponible en mode audio uniquement", "audio_mode_disabled": "Non disponible en mode audio uniquement",
"select_subtitles_first": "Veuillez d'abord sélectionner les sous-titres" "select_subtitles_first": "Veuillez d'abord sélectionner les sous-titres",
"settings_tooltip": "Chemin actuel : {path}\nLimite de vitesse : {speed_limit}",
"speed_limit_none": "Aucune",
"open_folder_error": "Impossible douvrir le dossier : {error}",
"time_range_set": "Section définie : {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Erreur : yt-dlp a échoué : {error}", "ytdlp_failed": "Erreur : yt-dlp a échoué : {error}",
"parse_failed": "Erreur : Échec de l'analyse de la sortie yt-dlp : {error}", "parse_failed": "Erreur : Échec de l'analyse de la sortie yt-dlp : {error}",
"analysis_failed": "Erreur : Échec de l'analyse : {error}", "analysis_failed": "Erreur : Échec de l'analyse : {error}",
"generic_error": "Erreur : {error}" "generic_error": "Erreur : {error}",
"download_failed_return_code_conflict": "Téléchargement échoué avec le code de retour {return_code}. Cela peut être dû à un conflit entre plusieurs installations de yt-dlp. Essayez de désinstaller toute version installée au niveau du système (par ex. via snap ou apt) puis redémarrez lapplication.",
"download_failed_return_code": "Téléchargement échoué avec le code de retour {return_code}",
"direct_command_error": "Erreur dans la commande directe : {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Mise à jour disponible", "title": "Mise à jour disponible",
@@ -442,7 +476,8 @@
"next_check": "Prochaine vérification : {time}", "next_check": "Prochaine vérification : {time}",
"next_check_error": "Prochaine vérification : Erreur de calcul", "next_check_error": "Prochaine vérification : Erreur de calcul",
"checking": "🔄 Vérification...", "checking": "🔄 Vérification...",
"check_now": "🔍 Vérifier les mises à jour maintenant" "check_now": "🔍 Vérifier les mises à jour maintenant",
"current_version": "Version actuelle de yt-dlp : {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "L'URL ne peut pas être vide", "empty_url": "L'URL ne peut pas être vide",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Êtes-vous sûr? Cela ne peut pas être annulé.", "clear_confirm_message": "Êtes-vous sûr? Cela ne peut pas être annulé.",
"no_history": "Pas encore d'historique", "no_history": "Pas encore d'historique",
"no_history_description": "Vos téléchargements apparaîtront ici", "no_history_description": "Vos téléchargements apparaîtront ici",
"loading": "Chargement de lhistorique...",
"search_placeholder": "Rechercher...", "search_placeholder": "Rechercher...",
"open_location": "Ouvrir l'Emplacement", "open_location": "Ouvrir l'Emplacement",
"redownload": "Télécharger à Nouveau", "redownload": "Télécharger à Nouveau",
@@ -491,7 +527,47 @@
"one_entry": "1 téléchargement", "one_entry": "1 téléchargement",
"redownload_confirm_title": "Télécharger à Nouveau?", "redownload_confirm_title": "Télécharger à Nouveau?",
"redownload_confirm_message": "Télécharger à nouveau?\n\n{title}", "redownload_confirm_message": "Télécharger à nouveau?\n\n{title}",
"redownload_started": "Téléchargement démarré" "redownload_started": "Téléchargement démarré",
"no_url_error": "Aucune URL trouvée dans lentrée dhistorique",
"redownload_failed": "Impossible de relancer le téléchargement : {error}"
},
"ffmpeg": {
"installation_title": "Installation de FFmpeg",
"installation_message": "YTSage a besoin de FFmpeg pour traiter les vidéos.\n\nChoisissez une option dinstallation cidessous :",
"install_button": "Installer FFmpeg",
"manual_guide": "Guide manuel",
"installation_failed": "Linstallation de FFmpeg a rencontré un problème.",
"already_installed": "FFmpeg est déjà installé !",
"installation_complete": "Installation terminée. Vous pouvez fermer cette boîte de dialogue et continuer à utiliser YTSage.",
"installing": "Installation de FFmpeg... Veuillez patienter",
"install_success": "FFmpeg a été installé avec succès !",
"installation_complete_close": "Installation terminée. Vous pouvez maintenant fermer cette boîte de dialogue et continuer à utiliser YTSage.",
"try_manual": "Veuillez essayer dutiliser le guide dinstallation manuel à la place."
},
"ytdlp_setup": {
"required_title": "Configuration de yt-dlp requise",
"description": "YTSage nécessite yt-dlp pour télécharger des vidéos.<br><br>yt-dlp est introuvable dans le répertoire local de lapplication. YTSage doit configurer yt-dlp pour votre système {os_name}.<br><br>Veuillez choisir une option cidessous :",
"option_auto": "Télécharger automatiquement (recommandé)",
"option_manual": "Sélectionner le chemin manuellement",
"setup_button": "Configurer yt-dlp",
"downloading": "Téléchargement de yt-dlp...",
"success": "yt-dlp a été installé avec succès !",
"error": "Erreur : {error}",
"download_failed_title": "Téléchargement échoué",
"download_failed_message": "Échec du téléchargement de yt-dlp : {error}",
"select_executable_title": "Sélectionner lexécutable yt-dlp",
"copied_to": "yt-dlp copié avec succès vers {path}",
"setup_error_title": "Erreur de configuration",
"copy_error": "Erreur lors de la copie de yt-dlp dans le répertoire de lapplication : {error}",
"invalid_executable_title": "Exécutable invalide",
"invalid_executable_message": "Le fichier sélectionné ne semble pas être un exécutable yt-dlp valide.",
"verify_error": "Erreur lors de la vérification de lexécutable yt-dlp : {error}",
"setup_failed_title": "Configuration échouée",
"setup_failed_message": "La configuration de yt-dlp a échoué. Certaines fonctionnalités peuvent ne pas fonctionner correctement.",
"success_dialog_title": "Configuration de yt-dlp",
"success_dialog_message": "yt-dlp a été configuré avec succès à lemplacement :\n{path}",
"file_filter_windows": "Fichiers exécutables (*.exe)",
"file_filter_all": "Tous les fichiers (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Vérificateur de Version FFmpeg", "title": "Vérificateur de Version FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "केवल ऑडियो" "audio_only_resolution": "केवल ऑडियो"
}, },
"buttons": { "buttons": {
"reset": "रीसेट",
"download": "डाउनलोड", "download": "डाउनलोड",
"pause": "रोकें", "pause": "रोकें",
"resume": "जारी रखें", "resume": "जारी रखें",
@@ -93,7 +94,8 @@
"select_subtitles": "उपशीर्षक चुनें", "select_subtitles": "उपशीर्षक चुनें",
"filter_languages_placeholder": "भाषाएं फ़िल्टर करें (जैसे: hi, en)...", "filter_languages_placeholder": "भाषाएं फ़िल्टर करें (जैसे: hi, en)...",
"no_subtitles_available": "कोई उपशीर्षक उपलब्ध नहीं", "no_subtitles_available": "कोई उपशीर्षक उपलब्ध नहीं",
"matching": "मेल खाता" "matching": "मेल खाता",
"ytdlp_log_title": "yt-dlp लॉग"
}, },
"tabs": { "tabs": {
"cookies": "कुकीज़ के साथ लॉगिन", "cookies": "कुकीज़ के साथ लॉगिन",
@@ -124,7 +126,10 @@
"browser_selected_title": "ब्राउज़र कुकीज़ लागू की गईं", "browser_selected_title": "ब्राउज़र कुकीज़ लागू की गईं",
"browser_applied_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}", "browser_applied_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}",
"cleared_title": "कुकीज़ साफ़ की गईं", "cleared_title": "कुकीज़ साफ़ की गईं",
"cleared_message": "कुकी सेटिंग्स साफ़ कर दी गई हैं" "cleared_message": "कुकी सेटिंग्स साफ़ कर दी गई हैं",
"active_browser": "✓ सक्रिय: ब्राउज़र कुकीज़ ({browser})",
"active_file": "✓ सक्रिय: कुकी फ़ाइल ({file})",
"none_active": "○ कोई कुकी सक्रिय नहीं"
}, },
"custom_command": { "custom_command": {
"help_text": "नीचे अपना कस्टम yt-dlp कमांड दर्ज करें। वर्तमान URL स्वचालित रूप से जोड़ा जाएगा।<br><br>विकल्पों की पूरी सूची और उपयोग के उदाहरणों के लिए <a href=\"{docs_url}\">आधिकारिक yt-dlp दस्तावेज़ देखने के लिए यहाँ क्लिक करें</a>।<br><br>नोट: डाउनलोड पथ और फ़ाइलनाम टेम्प्लेट स्वचालित रूप से संभाले जाते हैं।", "help_text": "नीचे अपना कस्टम yt-dlp कमांड दर्ज करें। वर्तमान URL स्वचालित रूप से जोड़ा जाएगा।<br><br>विकल्पों की पूरी सूची और उपयोग के उदाहरणों के लिए <a href=\"{docs_url}\">आधिकारिक yt-dlp दस्तावेज़ देखने के लिए यहाँ क्लिक करें</a>।<br><br>नोट: डाउनलोड पथ और फ़ाइलनाम टेम्प्लेट स्वचालित रूप से संभाले जाते हैं।",
@@ -137,7 +142,14 @@
"full_command": "🔧 पूरा कमांड: {command}", "full_command": "🔧 पूरा कमांड: {command}",
"command_success": "✅ कस्टम कमांड सफलतापूर्वक निष्पादित!", "command_success": "✅ कस्टम कमांड सफलतापूर्वक निष्पादित!",
"command_failed": "❌ कमांड असफल, एग्जिट कोड {code}", "command_failed": "❌ कमांड असफल, एग्जिट कोड {code}",
"command_error": "❌ कस्टम कमांड निष्पादित करने में त्रुटि: {error}" "command_error": "❌ कस्टम कमांड निष्पादित करने में त्रुटि: {error}",
"error_no_url": "❌ त्रुटि: कोई URL नहीं दिया गया। कृपया मुख्य विंडो में URL दर्ज करें।",
"error_no_command": "❌ त्रुटि: कोई कमांड नहीं दिया गया। कृपया yt-dlp आर्गुमेंट्स दर्ज करें।",
"executing": "🚀 कस्टम yt-dlp कमांड चल रहा है",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ आर्गुमेंट्स: {command}",
"download_path_label": "📁 डाउनलोड पथ: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "डाउनलोड के लिए प्रॉक्सी सेटिंग्स कॉन्फ़िगर करें। प्रत्यक्ष कनेक्शन के लिए खाली छोड़ें।", "help_text": "डाउनलोड के लिए प्रॉक्सी सेटिंग्स कॉन्फ़िगर करें। प्रत्यक्ष कनेक्शन के लिए खाली छोड़ें।",
@@ -159,7 +171,15 @@
"invalid_main_url": "अमान्य मुख्य प्रॉक्सी URL प्रारूप", "invalid_main_url": "अमान्य मुख्य प्रॉक्सी URL प्रारूप",
"invalid_geo_url": "अमान्य भू-प्रॉक्सी URL प्रारूप", "invalid_geo_url": "अमान्य भू-प्रॉक्सी URL प्रारूप",
"main_configured": "मुख्य प्रॉक्सी कॉन्फ़िगर की गई", "main_configured": "मुख्य प्रॉक्सी कॉन्फ़िगर की गई",
"geo_configured": "भू-प्रॉक्सी कॉन्फ़िगर की गई" "geo_configured": "भू-प्रॉक्सी कॉन्फ़िगर की गई",
"set_title": "प्रॉक्सी सेट",
"set_message": "मुख्य प्रॉक्सी सेट और सेव किया गया: {proxy}",
"geo_set_title": "जियो प्रॉक्सी सेट",
"geo_set_message": "जियो‑वेरिफिकेशन प्रॉक्सी सेट और सेव किया गया: {proxy}",
"cleared_title": "प्रॉक्सी सेटिंग्स साफ़ की गईं",
"cleared_message": "सभी प्रॉक्सी सेटिंग्स साफ़ कर सेव कर दी गईं।",
"saved_main": "सहेजा गया मुख्य प्रॉक्सी: {proxy}",
"saved_geo": "सहेजा गया जियो प्रॉक्सी: {proxy}"
}, },
"download": { "download": {
"preparing": "डाउनलोड तैयार हो रहा है...", "preparing": "डाउनलोड तैयार हो रहा है...",
@@ -225,6 +245,8 @@
"system_info": "सिस्टम जानकारी", "system_info": "सिस्टम जानकारी",
"loading": "🔄 सिस्टम जानकारी लोड हो रही है...", "loading": "🔄 सिस्टम जानकारी लोड हो रही है...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 लॉग्स",
"logs_tooltip": "एप्लिकेशन लॉग फ़ोल्डर खोलें",
"refreshing": "🔄 रिफ्रेश हो रहा है...", "refreshing": "🔄 रिफ्रेश हो रहा है...",
"refresh_failed": "रिफ्रेश असफल", "refresh_failed": "रिफ्रेश असफल",
"refresh_failed_message": "संस्करण जानकारी रिफ्रेश करने में असमर्थ।", "refresh_failed_message": "संस्करण जानकारी रिफ्रेश करने में असमर्थ।",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ सफलतापूर्वक {channel} चैनल पर स्विच किया गया!", "ytdlp_channel_switched": "✅ सफलतापूर्वक {channel} चैनल पर स्विच किया गया!",
"ytdlp_channel_switch_failed": "❌ चैनल स्विच करना विफल: {error}", "ytdlp_channel_switch_failed": "❌ चैनल स्विच करना विफल: {error}",
"ytdlp_current_channel": "वर्तमान चैनल: {channel}", "ytdlp_current_channel": "वर्तमान चैनल: {channel}",
"app_updates_title": "YTSage अपडेट",
"check_beta_updates": "बीटा अपडेट प्राप्त करें",
"auto_update_title": "स्वचालित अपडेट सेटिंग्स", "auto_update_title": "स्वचालित अपडेट सेटिंग्स",
"auto_update_header": "🔄 स्वचालित अपडेट सेटिंग्स", "auto_update_header": "🔄 स्वचालित अपडेट सेटिंग्स",
"auto_update_description": "yt-dlp के लिए स्वचालित अपडेट कॉन्फ़िगर करें ताकि आपके पास हमेशा नवीनतम सुविधाएं और बग फिक्स हों।", "auto_update_description": "yt-dlp के लिए स्वचालित अपडेट कॉन्फ़िगर करें ताकि आपके पास हमेशा नवीनतम सुविधाएं और बग फिक्स हों।",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (असंकुचित)", "audio_format_wav": "WAV (असंकुचित)",
"audio_format_opus": "Opus (कुशल)", "audio_format_opus": "Opus (कुशल)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (खुला)" "audio_format_vorbis": "Vorbis (खुला)",
"filename_format": "आउटपुट फ़ाइलनाम प्रारूप",
"filename_format_help": "उपलब्ध चर: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. मानक yt-dlp आउटपुट टेम्प्लेट सिंटैक्स समर्थित है."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "YouTube वीडियो या प्लेलिस्ट URL दर्ज करें", "url_placeholder": "YouTube वीडियो या प्लेलिस्ट URL दर्ज करें",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}", "browser_cookies_selected_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}",
"error_no_format_info": "त्रुटि: कोई प्रारूप जानकारी उपलब्ध नहीं।", "error_no_format_info": "त्रुटि: कोई प्रारूप जानकारी उपलब्ध नहीं।",
"error_extract_info": "त्रुटि: बुनियादी वीडियो जानकारी निकालने में असमर्थ। कृपया अपना लिंक जांचें।", "error_extract_info": "त्रुटि: बुनियादी वीडियो जानकारी निकालने में असमर्थ। कृपया अपना लिंक जांचें।",
"analyzing_preparing": "विश्लेषण (0%)... अनुरोध तैयार हो रहा है", "analyzing_preparing": "अनुरोध तैयार हो रहा है...",
"analyzing_extracting_basic": "विश्लेषण (15%)... बुनियादी जानकारी निकाली जा रही है", "analyzing_extracting_basic": "बुनियादी जानकारी निकाली जा रही है...",
"analyzing_extracting_detailed": "विश्लेषण (30%)... विस्तृत जानकारी निकाली जा रही है", "analyzing_extracting_detailed": "विस्तृत जानकारी निकाली जा रही है...",
"analyzing_processing_video": "विश्लेषण (45%)... वीडियो डेटा प्रोसेस हो रहा है", "analyzing_processing_video": "वीडियो डेटा प्रोसेस हो रहा है...",
"analyzing_processing_formats": "विश्लेषण (60%)... प्रारूप प्रोसेस हो रहे हैं", "analyzing_processing_formats": "प्रारूप प्रोसेस हो रहे हैं...",
"analyzing_loading_thumbnail": "विश्लेषण (75%)... थंबनेल लोड हो रहा है", "analyzing_loading_thumbnail": "थंबनेल लोड हो रहा है...",
"analyzing_processing_subtitles": "विश्लेषण (85%)... उपशीर्षक प्रोसेस हो रहे हैं", "analyzing_processing_subtitles": "उपशीर्षक प्रोसेस हो रहे हैं...",
"analyzing_updating_table": "विश्लेषण (95%)... प्रारूप तालिका अपडेट हो रही है", "analyzing_updating_table": "प्रारूप तालिका अपडेट हो रही है...",
"analysis_complete": "विश्लेषण पूर्ण!", "analysis_complete": "विश्लेषण पूर्ण!",
"analyzing_extracting_ytdlp": "विश्लेषण (30%)... जानकारी निकाली जा रही है", "analyzing_extracting_ytdlp": "जानकारी निकाली जा रही है...",
"analyzing_processing_data": "विश्लेषण (60%)... डेटा प्रोसेस हो रहा है", "analyzing_fetching_first_video": "पहले वीडियो के लिए प्रारूप प्राप्त कर रहा है...",
"analyzing_processing_formats_ytdlp": "विश्लेषण (75%)... प्रारूप प्रोसेस हो रह है", "analyzing_processing_data": "डेटा प्रोसेस हो रह है...",
"analyzing_loading_thumbnail_ytdlp": "विश्लेषण (85%)... थंबनेल लोड हो रह है", "analyzing_processing_formats_ytdlp": "प्रारूप प्रोसेस हो रह हैं...",
"analyzing_processing_subtitles_ytdlp": "विश्लेषण (90%)... उपशीर्षक प्रोसेस हो रह है", "analyzing_loading_thumbnail_ytdlp": "थंबनेल लोड हो रह है...",
"analyzing_processing_subtitles_ytdlp": "उपशीर्षक प्रोसेस हो रहे हैं...",
"select_subtitles": "उपशीर्षक चुनें...", "select_subtitles": "उपशीर्षक चुनें...",
"sponsorblock_categories": "SponsorBlock श्रेणियां...", "sponsorblock_categories": "SponsorBlock श्रेणियां...",
"invalid_url_or_enter": "अमान्य URL या कृपया URL दर्ज करें।", "invalid_url_or_enter": "अमान्य URL या कृपया URL दर्ज करें।",
"zero_selected": "0 चयनित", "zero_selected": "0 चयनित",
"analyze_first_tooltip": "कृपया पहले वीडियो का विश्लेषण करें", "analyze_first_tooltip": "कृपया पहले वीडियो का विश्लेषण करें",
"audio_mode_disabled": "केवल ऑडियो मोड में उपलब्ध नहीं", "audio_mode_disabled": "केवल ऑडियो मोड में उपलब्ध नहीं",
"select_subtitles_first": "कृपया पहले उपशीर्षक चुनें" "select_subtitles_first": "कृपया पहले उपशीर्षक चुनें",
"settings_tooltip": "वर्तमान पथ: {path}\nस्पीड लिमिट: {speed_limit}",
"speed_limit_none": "कोई नहीं",
"open_folder_error": "फ़ोल्डर नहीं खुल सका: {error}",
"time_range_set": "सेक्शन सेट: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "प्रायोजक", "sponsor": "प्रायोजक",
@@ -408,7 +439,10 @@
"ytdlp_failed": "त्रुटि: yt-dlp असफल: {error}", "ytdlp_failed": "त्रुटि: yt-dlp असफल: {error}",
"parse_failed": "त्रुटि: yt-dlp आउटपुट पार्स करने में विफल: {error}", "parse_failed": "त्रुटि: yt-dlp आउटपुट पार्स करने में विफल: {error}",
"analysis_failed": "त्रुटि: विश्लेषण विफल: {error}", "analysis_failed": "त्रुटि: विश्लेषण विफल: {error}",
"generic_error": "त्रुटि: {error}" "generic_error": "त्रुटि: {error}",
"download_failed_return_code_conflict": "डाउनलोड {return_code} रिटर्न कोड के साथ विफल हुआ। यह कई yt-dlp इंस्टॉलेशन के टकराव के कारण हो सकता है। किसी भी सिस्टम-इंस्टॉल्ड yt-dlp (जैसे snap या apt) को हटाकर ऐप को रीस्टार्ट करें।",
"download_failed_return_code": "डाउनलोड {return_code} रिटर्न कोड के साथ विफल हुआ",
"direct_command_error": "सीधे कमांड में त्रुटि: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "अपडेट उपलब्ध", "title": "अपडेट उपलब्ध",
@@ -442,7 +476,8 @@
"next_check": "अगली जांच: {time}", "next_check": "अगली जांच: {time}",
"next_check_error": "अगली जांच: गणना त्रुटि", "next_check_error": "अगली जांच: गणना त्रुटि",
"checking": "🔄 जांच रहे हैं...", "checking": "🔄 जांच रहे हैं...",
"check_now": "🔍 अभी अपडेट की जांच करें" "check_now": "🔍 अभी अपडेट की जांच करें",
"current_version": "वर्तमान yt-dlp संस्करण: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL खाली नहीं हो सकता", "empty_url": "URL खाली नहीं हो सकता",
@@ -473,6 +508,7 @@
"clear_confirm_message": "क्या आप निश्चित हैं? इसे पूर्ववत नहीं किया जा सकता।", "clear_confirm_message": "क्या आप निश्चित हैं? इसे पूर्ववत नहीं किया जा सकता।",
"no_history": "अभी तक कोई इतिहास नहीं", "no_history": "अभी तक कोई इतिहास नहीं",
"no_history_description": "आपके डाउनलोड यहां दिखाई देंगे", "no_history_description": "आपके डाउनलोड यहां दिखाई देंगे",
"loading": "इतिहास लोड हो रहा है...",
"search_placeholder": "खोजें...", "search_placeholder": "खोजें...",
"open_location": "स्थान खोलें", "open_location": "स्थान खोलें",
"redownload": "फिर से डाउनलोड करें", "redownload": "फिर से डाउनलोड करें",
@@ -491,7 +527,47 @@
"one_entry": "1 डाउनलोड", "one_entry": "1 डाउनलोड",
"redownload_confirm_title": "फिर से डाउनलोड करें?", "redownload_confirm_title": "फिर से डाउनलोड करें?",
"redownload_confirm_message": "फिर से डाउनलोड करें?\n\n{title}", "redownload_confirm_message": "फिर से डाउनलोड करें?\n\n{title}",
"redownload_started": "डाउनलोड शुरू हुआ" "redownload_started": "डाउनलोड शुरू हुआ",
"no_url_error": "हिस्ट्री एंट्री में कोई URL नहीं मिला",
"redownload_failed": "रीडाउनलोड शुरू करने में विफल: {error}"
},
"ffmpeg": {
"installation_title": "FFmpeg इंस्टॉलेशन",
"installation_message": "वीडियो प्रोसेस करने के लिए YTSage को FFmpeg चाहिए।\n\nनीचे एक इंस्टॉलेशन विकल्प चुनें:",
"install_button": "FFmpeg इंस्टॉल करें",
"manual_guide": "मैन्युअल गाइड",
"installation_failed": "FFmpeg इंस्टॉलेशन में समस्या आई।",
"already_installed": "FFmpeg पहले से इंस्टॉल है!",
"installation_complete": "इंस्टॉलेशन पूरा हुआ। आप इस डायलॉग को बंद कर YTSage उपयोग कर सकते हैं।",
"installing": "FFmpeg इंस्टॉल हो रहा है... कृपया प्रतीक्षा करें",
"install_success": "FFmpeg सफलतापूर्वक इंस्टॉल हो गया!",
"installation_complete_close": "इंस्टॉलेशन पूरा हुआ। अब आप इस डायलॉग को बंद कर YTSage उपयोग कर सकते हैं।",
"try_manual": "कृपया इसके बजाय मैन्युअल इंस्टॉलेशन गाइड का उपयोग करें।"
},
"ytdlp_setup": {
"required_title": "yt-dlp सेटअप आवश्यक",
"description": "वीडियो डाउनलोड करने के लिए YTSage को yt-dlp चाहिए।<br><br>ऐप की लोकल डायरेक्टरी में yt-dlp नहीं मिला। YTSage को आपके {os_name} सिस्टम के लिए yt-dlp सेटअप करना होगा।<br><br>कृपया नीचे एक विकल्प चुनें:",
"option_auto": "अपने आप डाउनलोड करें (सिफ़ारिश की गई)",
"option_manual": "पाथ मैन्युअली चुनें",
"setup_button": "yt-dlp सेटअप",
"downloading": "yt-dlp डाउनलोड हो रहा है...",
"success": "yt-dlp सफलतापूर्वक इंस्टॉल हो गया!",
"error": "त्रुटि: {error}",
"download_failed_title": "डाउनलोड विफल",
"download_failed_message": "yt-dlp डाउनलोड नहीं हो सका: {error}",
"select_executable_title": "yt-dlp executable चुनें",
"copied_to": "yt-dlp को {path} पर सफलतापूर्वक कॉपी किया गया",
"setup_error_title": "सेटअप त्रुटि",
"copy_error": "yt-dlp को ऐप डायरेक्टरी में कॉपी करने में त्रुटि: {error}",
"invalid_executable_title": "अमान्य executable",
"invalid_executable_message": "चुनी गई फ़ाइल वैध yt-dlp executable नहीं लगती।",
"verify_error": "yt-dlp executable सत्यापित करने में त्रुटि: {error}",
"setup_failed_title": "सेटअप विफल",
"setup_failed_message": "yt-dlp सेटअप विफल रहा। कुछ फीचर सही से काम नहीं कर सकते।",
"success_dialog_title": "yt-dlp सेटअप",
"success_dialog_message": "yt-dlp सफलतापूर्वक इस स्थान पर कॉन्फ़िगर हुआ है:\n{path}",
"file_filter_windows": "Executable फ़ाइलें (*.exe)",
"file_filter_all": "सभी फ़ाइलें (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "FFmpeg संस्करण जांचकर्ता", "title": "FFmpeg संस्करण जांचकर्ता",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Audio saja" "audio_only_resolution": "Audio saja"
}, },
"buttons": { "buttons": {
"reset": "Atur Ulang",
"download": "Unduh", "download": "Unduh",
"pause": "Jeda", "pause": "Jeda",
"resume": "Lanjutkan", "resume": "Lanjutkan",
@@ -93,7 +94,8 @@
"select_subtitles": "Pilih subtitle", "select_subtitles": "Pilih subtitle",
"filter_languages_placeholder": "Filter bahasa (misalnya: id, en)...", "filter_languages_placeholder": "Filter bahasa (misalnya: id, en)...",
"no_subtitles_available": "Tidak ada subtitle yang tersedia", "no_subtitles_available": "Tidak ada subtitle yang tersedia",
"matching": "yang cocok" "matching": "yang cocok",
"ytdlp_log_title": "Log yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Masuk dengan cookies", "cookies": "Masuk dengan cookies",
@@ -124,7 +126,10 @@
"browser_selected_title": "Cookies Browser Diterapkan", "browser_selected_title": "Cookies Browser Diterapkan",
"browser_applied_message": "Cookies browser akan diekstrak dari: {browser}", "browser_applied_message": "Cookies browser akan diekstrak dari: {browser}",
"cleared_title": "Cookies Dihapus", "cleared_title": "Cookies Dihapus",
"cleared_message": "Pengaturan cookie telah dihapus" "cleared_message": "Pengaturan cookie telah dihapus",
"active_browser": "✓ Aktif: cookie browser ({browser})",
"active_file": "✓ Aktif: file cookie ({file})",
"none_active": "○ Tidak ada cookie aktif"
}, },
"custom_command": { "custom_command": {
"help_text": "Masukkan perintah yt-dlp khusus Anda di bawah. URL saat ini akan ditambahkan secara otomatis.<br><br>Untuk daftar lengkap opsi dan contoh penggunaan <a href=\"{docs_url}\">klik di sini untuk melihat dokumentasi resmi yt-dlp</a>.<br><br>Catatan: Jalur unduhan dan template nama file ditangani secara otomatis.", "help_text": "Masukkan perintah yt-dlp khusus Anda di bawah. URL saat ini akan ditambahkan secara otomatis.<br><br>Untuk daftar lengkap opsi dan contoh penggunaan <a href=\"{docs_url}\">klik di sini untuk melihat dokumentasi resmi yt-dlp</a>.<br><br>Catatan: Jalur unduhan dan template nama file ditangani secara otomatis.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Perintah lengkap: {command}", "full_command": "🔧 Perintah lengkap: {command}",
"command_success": "✅ Perintah khusus berhasil dijalankan!", "command_success": "✅ Perintah khusus berhasil dijalankan!",
"command_failed": "❌ Perintah gagal dengan kode keluar {code}", "command_failed": "❌ Perintah gagal dengan kode keluar {code}",
"command_error": "❌ Kesalahan menjalankan perintah khusus: {error}" "command_error": "❌ Kesalahan menjalankan perintah khusus: {error}",
"error_no_url": "❌ Error: Tidak ada URL yang diberikan. Masukkan URL di jendela utama.",
"error_no_command": "❌ Error: Tidak ada perintah yang diberikan. Masukkan argumen yt-dlp.",
"executing": "🚀 Menjalankan perintah yt-dlp kustom",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argumen: {command}",
"download_path_label": "📁 Lokasi unduhan: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Konfigurasi pengaturan proxy untuk unduhan. Biarkan kosong untuk koneksi langsung.", "help_text": "Konfigurasi pengaturan proxy untuk unduhan. Biarkan kosong untuk koneksi langsung.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Format URL proxy utama tidak valid", "invalid_main_url": "Format URL proxy utama tidak valid",
"invalid_geo_url": "Format URL proxy geografis tidak valid", "invalid_geo_url": "Format URL proxy geografis tidak valid",
"main_configured": "Proxy utama dikonfigurasi", "main_configured": "Proxy utama dikonfigurasi",
"geo_configured": "Proxy geografis dikonfigurasi" "geo_configured": "Proxy geografis dikonfigurasi",
"set_title": "Proxy diatur",
"set_message": "Proxy utama diatur dan disimpan: {proxy}",
"geo_set_title": "Proxy geo diatur",
"geo_set_message": "Proxy verifikasi geo diatur dan disimpan: {proxy}",
"cleared_title": "Pengaturan proxy dibersihkan",
"cleared_message": "Semua pengaturan proxy telah dibersihkan dan disimpan.",
"saved_main": "Proxy utama tersimpan: {proxy}",
"saved_geo": "Proxy geo tersimpan: {proxy}"
}, },
"download": { "download": {
"preparing": "Mempersiapkan unduhan...", "preparing": "Mempersiapkan unduhan...",
@@ -225,6 +245,8 @@
"system_info": "Informasi sistem", "system_info": "Informasi sistem",
"loading": "🔄 Memuat informasi sistem...", "loading": "🔄 Memuat informasi sistem...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Log",
"logs_tooltip": "Buka folder log aplikasi",
"refreshing": "🔄 Menyegarkan...", "refreshing": "🔄 Menyegarkan...",
"refresh_failed": "Gagal menyegarkan", "refresh_failed": "Gagal menyegarkan",
"refresh_failed_message": "Tidak dapat menyegarkan informasi versi.", "refresh_failed_message": "Tidak dapat menyegarkan informasi versi.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Berhasil beralih ke saluran {channel}!", "ytdlp_channel_switched": "✅ Berhasil beralih ke saluran {channel}!",
"ytdlp_channel_switch_failed": "❌ Gagal beralih saluran: {error}", "ytdlp_channel_switch_failed": "❌ Gagal beralih saluran: {error}",
"ytdlp_current_channel": "Saluran saat ini: {channel}", "ytdlp_current_channel": "Saluran saat ini: {channel}",
"app_updates_title": "Pembaruan YTSage",
"check_beta_updates": "Terima Pembaruan Beta",
"auto_update_title": "Pengaturan pembaruan otomatis", "auto_update_title": "Pengaturan pembaruan otomatis",
"auto_update_header": "🔄 Pengaturan pembaruan otomatis", "auto_update_header": "🔄 Pengaturan pembaruan otomatis",
"auto_update_description": "Konfigurasi pembaruan otomatis untuk yt-dlp untuk memastikan Anda selalu memiliki fitur dan perbaikan bug terbaru.", "auto_update_description": "Konfigurasi pembaruan otomatis untuk yt-dlp untuk memastikan Anda selalu memiliki fitur dan perbaikan bug terbaru.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Tidak terkompresi)", "audio_format_wav": "WAV (Tidak terkompresi)",
"audio_format_opus": "Opus (Efisien)", "audio_format_opus": "Opus (Efisien)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Terbuka)" "audio_format_vorbis": "Vorbis (Terbuka)",
"filename_format": "Format Nama File Keluaran",
"filename_format_help": "Variabel yang tersedia: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Sintaks templat keluaran standar yt-dlp didukung."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Masukkan URL video atau playlist YouTube", "url_placeholder": "Masukkan URL video atau playlist YouTube",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "Cookies browser akan diekstrak dari: {browser}", "browser_cookies_selected_message": "Cookies browser akan diekstrak dari: {browser}",
"error_no_format_info": "Kesalahan: Tidak ada informasi format yang tersedia.", "error_no_format_info": "Kesalahan: Tidak ada informasi format yang tersedia.",
"error_extract_info": "Kesalahan: Tidak dapat mengekstrak informasi video dasar. Silakan periksa tautan Anda.", "error_extract_info": "Kesalahan: Tidak dapat mengekstrak informasi video dasar. Silakan periksa tautan Anda.",
"analyzing_preparing": "Menganalisis (0%)... Mempersiapkan permintaan", "analyzing_preparing": "Mempersiapkan permintaan...",
"analyzing_extracting_basic": "Menganalisis (15%)... Mengekstrak informasi dasar", "analyzing_extracting_basic": "Mengekstrak informasi dasar...",
"analyzing_extracting_detailed": "Menganalisis (30%)... Mengekstrak informasi detail", "analyzing_extracting_detailed": "Mengekstrak informasi detail...",
"analyzing_processing_video": "Menganalisis (45%)... Memproses data video", "analyzing_processing_video": "Memproses data video...",
"analyzing_processing_formats": "Menganalisis (60%)... Memproses format", "analyzing_processing_formats": "Memproses format...",
"analyzing_loading_thumbnail": "Menganalisis (75%)... Memuat thumbnail", "analyzing_loading_thumbnail": "Memuat thumbnail...",
"analyzing_processing_subtitles": "Menganalisis (85%)... Memproses subtitle", "analyzing_processing_subtitles": "Memproses subtitle...",
"analyzing_updating_table": "Menganalisis (95%)... Memperbarui tabel format", "analyzing_updating_table": "Memperbarui tabel format...",
"analysis_complete": "Analisis selesai!", "analysis_complete": "Analisis selesai!",
"analyzing_extracting_ytdlp": "Menganalisis (30%)... Mengekstrak informasi", "analyzing_extracting_ytdlp": "Mengekstrak informasi...",
"analyzing_processing_data": "Menganalisis (60%)... Memproses data", "analyzing_fetching_first_video": "Mengambil format untuk video pertama...",
"analyzing_processing_formats_ytdlp": "Menganalisis (75%)... Memproses format", "analyzing_processing_data": "Memproses data...",
"analyzing_loading_thumbnail_ytdlp": "Menganalisis (85%)... Memuat thumbnail", "analyzing_processing_formats_ytdlp": "Memproses format...",
"analyzing_processing_subtitles_ytdlp": "Menganalisis (90%)... Memproses subtitle", "analyzing_loading_thumbnail_ytdlp": "Memuat thumbnail...",
"analyzing_processing_subtitles_ytdlp": "Memproses subtitle...",
"select_subtitles": "Pilih subtitle...", "select_subtitles": "Pilih subtitle...",
"sponsorblock_categories": "Kategori SponsorBlock...", "sponsorblock_categories": "Kategori SponsorBlock...",
"invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.", "invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.",
"zero_selected": "0 dipilih", "zero_selected": "0 dipilih",
"analyze_first_tooltip": "Silakan analisis video terlebih dahulu", "analyze_first_tooltip": "Silakan analisis video terlebih dahulu",
"audio_mode_disabled": "Tidak tersedia dalam mode audio saja", "audio_mode_disabled": "Tidak tersedia dalam mode audio saja",
"select_subtitles_first": "Silakan pilih subtitle terlebih dahulu" "select_subtitles_first": "Silakan pilih subtitle terlebih dahulu",
"settings_tooltip": "Path saat ini: {path}\nBatas kecepatan: {speed_limit}",
"speed_limit_none": "Tidak ada",
"open_folder_error": "Tidak dapat membuka folder: {error}",
"time_range_set": "Bagian disetel: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Kesalahan: yt-dlp gagal: {error}", "ytdlp_failed": "Kesalahan: yt-dlp gagal: {error}",
"parse_failed": "Kesalahan: Gagal mengurai output yt-dlp: {error}", "parse_failed": "Kesalahan: Gagal mengurai output yt-dlp: {error}",
"analysis_failed": "Kesalahan: Analisis gagal: {error}", "analysis_failed": "Kesalahan: Analisis gagal: {error}",
"generic_error": "Kesalahan: {error}" "generic_error": "Kesalahan: {error}",
"download_failed_return_code_conflict": "Unduhan gagal dengan kode pengembalian {return_code}. Ini mungkin karena konflik dengan beberapa instalasi yt-dlp. Coba uninstall yt-dlp yang terpasang di sistem (mis. melalui snap atau apt) lalu mulai ulang aplikasi.",
"download_failed_return_code": "Unduhan gagal dengan kode pengembalian {return_code}",
"direct_command_error": "Error pada perintah langsung: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Pembaruan Tersedia", "title": "Pembaruan Tersedia",
@@ -442,7 +476,8 @@
"next_check": "Pemeriksaan berikutnya: {time}", "next_check": "Pemeriksaan berikutnya: {time}",
"next_check_error": "Pemeriksaan berikutnya: Kesalahan perhitungan", "next_check_error": "Pemeriksaan berikutnya: Kesalahan perhitungan",
"checking": "🔄 Memeriksa...", "checking": "🔄 Memeriksa...",
"check_now": "🔍 Periksa pembaruan sekarang" "check_now": "🔍 Periksa pembaruan sekarang",
"current_version": "Versi yt-dlp saat ini: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL tidak boleh kosong", "empty_url": "URL tidak boleh kosong",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Apakah Anda yakin? Ini tidak dapat dibatalkan.", "clear_confirm_message": "Apakah Anda yakin? Ini tidak dapat dibatalkan.",
"no_history": "Belum ada riwayat", "no_history": "Belum ada riwayat",
"no_history_description": "Unduhan Anda akan muncul di sini", "no_history_description": "Unduhan Anda akan muncul di sini",
"loading": "Memuat riwayat...",
"search_placeholder": "Cari...", "search_placeholder": "Cari...",
"open_location": "Buka Lokasi", "open_location": "Buka Lokasi",
"redownload": "Unduh Lagi", "redownload": "Unduh Lagi",
@@ -491,7 +527,47 @@
"one_entry": "1 unduhan", "one_entry": "1 unduhan",
"redownload_confirm_title": "Unduh Lagi?", "redownload_confirm_title": "Unduh Lagi?",
"redownload_confirm_message": "Unduh lagi?\n\n{title}", "redownload_confirm_message": "Unduh lagi?\n\n{title}",
"redownload_started": "Unduhan dimulai" "redownload_started": "Unduhan dimulai",
"no_url_error": "Tidak ada URL pada entri riwayat",
"redownload_failed": "Gagal memulai unduh ulang: {error}"
},
"ffmpeg": {
"installation_title": "Instalasi FFmpeg",
"installation_message": "YTSage membutuhkan FFmpeg untuk memproses video.\n\nPilih opsi instalasi di bawah:",
"install_button": "Instal FFmpeg",
"manual_guide": "Panduan manual",
"installation_failed": "Instalasi FFmpeg mengalami masalah.",
"already_installed": "FFmpeg sudah terinstal!",
"installation_complete": "Instalasi selesai. Anda dapat menutup dialog ini dan melanjutkan menggunakan YTSage.",
"installing": "Menginstal FFmpeg... Harap tunggu",
"install_success": "FFmpeg berhasil diinstal!",
"installation_complete_close": "Instalasi selesai. Anda sekarang dapat menutup dialog ini dan melanjutkan menggunakan YTSage.",
"try_manual": "Silakan coba gunakan panduan instalasi manual sebagai gantinya."
},
"ytdlp_setup": {
"required_title": "Pengaturan yt-dlp diperlukan",
"description": "YTSage memerlukan yt-dlp untuk mengunduh video.<br><br>yt-dlp tidak ditemukan di direktori lokal aplikasi. YTSage perlu menyiapkan yt-dlp untuk sistem {os_name} Anda.<br><br>Silakan pilih opsi di bawah:",
"option_auto": "Unduh otomatis (disarankan)",
"option_manual": "Pilih path secara manual",
"setup_button": "Siapkan yt-dlp",
"downloading": "Mengunduh yt-dlp...",
"success": "yt-dlp berhasil diinstal!",
"error": "Error: {error}",
"download_failed_title": "Unduhan gagal",
"download_failed_message": "Gagal mengunduh yt-dlp: {error}",
"select_executable_title": "Pilih executable yt-dlp",
"copied_to": "yt-dlp berhasil disalin ke {path}",
"setup_error_title": "Kesalahan pengaturan",
"copy_error": "Kesalahan menyalin yt-dlp ke direktori aplikasi: {error}",
"invalid_executable_title": "Executable tidak valid",
"invalid_executable_message": "File yang dipilih tidak tampak sebagai executable yt-dlp yang valid.",
"verify_error": "Kesalahan memverifikasi executable yt-dlp: {error}",
"setup_failed_title": "Pengaturan gagal",
"setup_failed_message": "Gagal menyiapkan yt-dlp. Beberapa fitur mungkin tidak berfungsi dengan benar.",
"success_dialog_title": "Pengaturan yt-dlp",
"success_dialog_message": "yt-dlp berhasil dikonfigurasi di:\n{path}",
"file_filter_windows": "File executable (*.exe)",
"file_filter_all": "Semua file (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Pemeriksa Versi FFmpeg", "title": "Pemeriksa Versi FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Solo audio" "audio_only_resolution": "Solo audio"
}, },
"buttons": { "buttons": {
"reset": "Reimposta",
"download": "Scarica", "download": "Scarica",
"pause": "Pausa", "pause": "Pausa",
"resume": "Riprendi", "resume": "Riprendi",
@@ -93,7 +94,8 @@
"select_subtitles": "Seleziona sottotitoli", "select_subtitles": "Seleziona sottotitoli",
"filter_languages_placeholder": "Filtra lingue (es: it, en)...", "filter_languages_placeholder": "Filtra lingue (es: it, en)...",
"no_subtitles_available": "Nessun sottotitolo disponibile", "no_subtitles_available": "Nessun sottotitolo disponibile",
"matching": "corrispondenti" "matching": "corrispondenti",
"ytdlp_log_title": "Registro yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Accedi con i cookie", "cookies": "Accedi con i cookie",
@@ -124,7 +126,10 @@
"browser_selected_title": "Cookie del Browser Applicati", "browser_selected_title": "Cookie del Browser Applicati",
"browser_applied_message": "I cookie del browser verranno estratti da: {browser}", "browser_applied_message": "I cookie del browser verranno estratti da: {browser}",
"cleared_title": "Cookie Cancellati", "cleared_title": "Cookie Cancellati",
"cleared_message": "Le impostazioni dei cookie sono state cancellate" "cleared_message": "Le impostazioni dei cookie sono state cancellate",
"active_browser": "✓ Attivi: cookie del browser ({browser})",
"active_file": "✓ Attivo: file cookie ({file})",
"none_active": "○ Nessun cookie attivo"
}, },
"custom_command": { "custom_command": {
"help_text": "Inserisci un comando yt-dlp personalizzato qui sotto. L'URL corrente verrà aggiunto automaticamente.<br><br>Per l'elenco completo delle opzioni ed esempi di utilizzo <a href=\"{docs_url}\">clicca qui per vedere la documentazione ufficiale yt-dlp</a>.<br><br>Nota: Il percorso di download e il modello del nome file sono gestiti automaticamente.", "help_text": "Inserisci un comando yt-dlp personalizzato qui sotto. L'URL corrente verrà aggiunto automaticamente.<br><br>Per l'elenco completo delle opzioni ed esempi di utilizzo <a href=\"{docs_url}\">clicca qui per vedere la documentazione ufficiale yt-dlp</a>.<br><br>Nota: Il percorso di download e il modello del nome file sono gestiti automaticamente.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Comando completo: {command}", "full_command": "🔧 Comando completo: {command}",
"command_success": "✅ Comando personalizzato eseguito con successo!", "command_success": "✅ Comando personalizzato eseguito con successo!",
"command_failed": "❌ Comando fallito con codice di uscita {code}", "command_failed": "❌ Comando fallito con codice di uscita {code}",
"command_error": "❌ Errore nell'esecuzione del comando personalizzato: {error}" "command_error": "❌ Errore nell'esecuzione del comando personalizzato: {error}",
"error_no_url": "❌ Errore: nessun URL fornito. Inserisci un URL nella finestra principale.",
"error_no_command": "❌ Errore: nessun comando fornito. Inserisci gli argomenti di yt-dlp.",
"executing": "🚀 Esecuzione del comando yt-dlp personalizzato",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argomenti: {command}",
"download_path_label": "📁 Percorso di download: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Configura le impostazioni proxy per i download. Lascia vuoto per connessione diretta.", "help_text": "Configura le impostazioni proxy per i download. Lascia vuoto per connessione diretta.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Formato URL proxy principale non valido", "invalid_main_url": "Formato URL proxy principale non valido",
"invalid_geo_url": "Formato URL proxy geo non valido", "invalid_geo_url": "Formato URL proxy geo non valido",
"main_configured": "Proxy principale configurato", "main_configured": "Proxy principale configurato",
"geo_configured": "Proxy geo configurato" "geo_configured": "Proxy geo configurato",
"set_title": "Proxy impostato",
"set_message": "Proxy principale impostato e salvato: {proxy}",
"geo_set_title": "Proxy geo impostato",
"geo_set_message": "Proxy di verifica geo impostato e salvato: {proxy}",
"cleared_title": "Impostazioni proxy cancellate",
"cleared_message": "Tutte le impostazioni proxy sono state cancellate e salvate.",
"saved_main": "Proxy principale salvato: {proxy}",
"saved_geo": "Proxy geo salvato: {proxy}"
}, },
"download": { "download": {
"preparing": "Preparazione download...", "preparing": "Preparazione download...",
@@ -225,6 +245,8 @@
"system_info": "Informazioni sistema", "system_info": "Informazioni sistema",
"loading": "🔄 Caricamento informazioni sistema...", "loading": "🔄 Caricamento informazioni sistema...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Log",
"logs_tooltip": "Apri cartella log applicazione",
"refreshing": "🔄 Aggiornamento...", "refreshing": "🔄 Aggiornamento...",
"refresh_failed": "Aggiornamento fallito", "refresh_failed": "Aggiornamento fallito",
"refresh_failed_message": "Impossibile aggiornare le informazioni sulla versione.", "refresh_failed_message": "Impossibile aggiornare le informazioni sulla versione.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Passaggio al canale {channel} riuscito!", "ytdlp_channel_switched": "✅ Passaggio al canale {channel} riuscito!",
"ytdlp_channel_switch_failed": "❌ Impossibile cambiare canale: {error}", "ytdlp_channel_switch_failed": "❌ Impossibile cambiare canale: {error}",
"ytdlp_current_channel": "Canale attuale: {channel}", "ytdlp_current_channel": "Canale attuale: {channel}",
"app_updates_title": "Aggiornamenti YTSage",
"check_beta_updates": "Ricevi aggiornamenti beta",
"auto_update_title": "Impostazioni aggiornamenti automatici", "auto_update_title": "Impostazioni aggiornamenti automatici",
"auto_update_header": "🔄 Impostazioni aggiornamenti automatici", "auto_update_header": "🔄 Impostazioni aggiornamenti automatici",
"auto_update_description": "Configura gli aggiornamenti automatici per yt-dlp per garantire le ultime funzionalità e correzioni bug.", "auto_update_description": "Configura gli aggiornamenti automatici per yt-dlp per garantire le ultime funzionalità e correzioni bug.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Non compresso)", "audio_format_wav": "WAV (Non compresso)",
"audio_format_opus": "Opus (Efficiente)", "audio_format_opus": "Opus (Efficiente)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Aperto)" "audio_format_vorbis": "Vorbis (Aperto)",
"filename_format": "Formato nome file output",
"filename_format_help": "Variabili disponibili: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. È supportata la sintassi standard del modello di output di yt-dlp."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Inserisci URL video YouTube o playlist", "url_placeholder": "Inserisci URL video YouTube o playlist",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "I cookie del browser verranno estratti da: {browser}", "browser_cookies_selected_message": "I cookie del browser verranno estratti da: {browser}",
"error_no_format_info": "Errore: Nessuna informazione formato disponibile.", "error_no_format_info": "Errore: Nessuna informazione formato disponibile.",
"error_extract_info": "Errore: Impossibile estrarre informazioni base del video. Controlla il tuo link.", "error_extract_info": "Errore: Impossibile estrarre informazioni base del video. Controlla il tuo link.",
"analyzing_preparing": "Analisi (0%)... Preparazione richiesta", "analyzing_preparing": "Preparazione richiesta...",
"analyzing_extracting_basic": "Analisi (15%)... Estrazione informazioni base", "analyzing_extracting_basic": "Estrazione informazioni base...",
"analyzing_extracting_detailed": "Analisi (30%)... Estrazione informazioni dettagliate", "analyzing_extracting_detailed": "Estrazione informazioni dettagliate...",
"analyzing_processing_video": "Analisi (45%)... Elaborazione dati video", "analyzing_processing_video": "Elaborazione dati video...",
"analyzing_processing_formats": "Analisi (60%)... Elaborazione formati", "analyzing_processing_formats": "Elaborazione formati...",
"analyzing_loading_thumbnail": "Analisi (75%)... Caricamento miniatura", "analyzing_loading_thumbnail": "Caricamento miniatura...",
"analyzing_processing_subtitles": "Analisi (85%)... Elaborazione sottotitoli", "analyzing_processing_subtitles": "Elaborazione sottotitoli...",
"analyzing_updating_table": "Analisi (95%)... Aggiornamento tabella formati", "analyzing_updating_table": "Aggiornamento tabella formati...",
"analysis_complete": "Analisi completata!", "analysis_complete": "Analisi completata!",
"analyzing_extracting_ytdlp": "Analisi (30%)... Estrazione informazioni", "analyzing_extracting_ytdlp": "Estrazione informazioni...",
"analyzing_processing_data": "Analisi (60%)... Elaborazione dati", "analyzing_fetching_first_video": "Recupero dei formati per il primo video...",
"analyzing_processing_formats_ytdlp": "Analisi (75%)... Elaborazione formati", "analyzing_processing_data": "Elaborazione dati...",
"analyzing_loading_thumbnail_ytdlp": "Analisi (85%)... Caricamento miniatura", "analyzing_processing_formats_ytdlp": "Elaborazione formati...",
"analyzing_processing_subtitles_ytdlp": "Analisi (90%)... Elaborazione sottotitoli", "analyzing_loading_thumbnail_ytdlp": "Caricamento miniatura...",
"analyzing_processing_subtitles_ytdlp": "Elaborazione sottotitoli...",
"select_subtitles": "Seleziona sottotitoli...", "select_subtitles": "Seleziona sottotitoli...",
"sponsorblock_categories": "Categorie SponsorBlock...", "sponsorblock_categories": "Categorie SponsorBlock...",
"invalid_url_or_enter": "URL non valido o inserisci un URL.", "invalid_url_or_enter": "URL non valido o inserisci un URL.",
"zero_selected": "0 selezionati", "zero_selected": "0 selezionati",
"analyze_first_tooltip": "Si prega di analizzare prima il video", "analyze_first_tooltip": "Si prega di analizzare prima il video",
"audio_mode_disabled": "Non disponibile in modalità solo audio", "audio_mode_disabled": "Non disponibile in modalità solo audio",
"select_subtitles_first": "Si prega di selezionare prima i sottotitoli" "select_subtitles_first": "Si prega di selezionare prima i sottotitoli",
"settings_tooltip": "Percorso corrente: {path}\nLimite velocità: {speed_limit}",
"speed_limit_none": "Nessuno",
"open_folder_error": "Impossibile aprire la cartella: {error}",
"time_range_set": "Sezione impostata: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Errore: yt-dlp fallito: {error}", "ytdlp_failed": "Errore: yt-dlp fallito: {error}",
"parse_failed": "Errore: Impossibile analizzare l'output di yt-dlp: {error}", "parse_failed": "Errore: Impossibile analizzare l'output di yt-dlp: {error}",
"analysis_failed": "Errore: Analisi fallita: {error}", "analysis_failed": "Errore: Analisi fallita: {error}",
"generic_error": "Errore: {error}" "generic_error": "Errore: {error}",
"download_failed_return_code_conflict": "Download non riuscito con codice di uscita {return_code}. Potrebbe essere dovuto a un conflitto tra più installazioni di yt-dlp. Prova a disinstallare eventuali yt-dlp installati a livello di sistema (es. snap o apt) e riavvia lapp.",
"download_failed_return_code": "Download non riuscito con codice di uscita {return_code}",
"direct_command_error": "Errore nel comando diretto: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Aggiornamento disponibile", "title": "Aggiornamento disponibile",
@@ -442,7 +476,8 @@
"next_check": "Prossimo controllo: {time}", "next_check": "Prossimo controllo: {time}",
"next_check_error": "Prossimo controllo: Errore di calcolo", "next_check_error": "Prossimo controllo: Errore di calcolo",
"checking": "🔄 Controllo...", "checking": "🔄 Controllo...",
"check_now": "🔍 Controlla aggiornamenti ora" "check_now": "🔍 Controlla aggiornamenti ora",
"current_version": "Versione corrente di yt-dlp: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "L'URL non può essere vuoto", "empty_url": "L'URL non può essere vuoto",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Sei sicuro? Questa azione non può essere annullata.", "clear_confirm_message": "Sei sicuro? Questa azione non può essere annullata.",
"no_history": "Nessuna cronologia ancora", "no_history": "Nessuna cronologia ancora",
"no_history_description": "I tuoi download appariranno qui", "no_history_description": "I tuoi download appariranno qui",
"loading": "Caricamento cronologia...",
"search_placeholder": "Cerca...", "search_placeholder": "Cerca...",
"open_location": "Apri Posizione", "open_location": "Apri Posizione",
"redownload": "Scarica di Nuovo", "redownload": "Scarica di Nuovo",
@@ -491,7 +527,47 @@
"one_entry": "1 download", "one_entry": "1 download",
"redownload_confirm_title": "Scaricare di Nuovo?", "redownload_confirm_title": "Scaricare di Nuovo?",
"redownload_confirm_message": "Scaricare di nuovo?\n\n{title}", "redownload_confirm_message": "Scaricare di nuovo?\n\n{title}",
"redownload_started": "Download avviato" "redownload_started": "Download avviato",
"no_url_error": "Nessun URL trovato nella voce della cronologia",
"redownload_failed": "Impossibile avviare il download di nuovo: {error}"
},
"ffmpeg": {
"installation_title": "Installazione di FFmpeg",
"installation_message": "YTSage richiede FFmpeg per elaborare i video.\n\nScegli un'opzione di installazione qui sotto:",
"install_button": "Installa FFmpeg",
"manual_guide": "Guida manuale",
"installation_failed": "L'installazione di FFmpeg ha riscontrato un problema.",
"already_installed": "FFmpeg è già installato!",
"installation_complete": "Installazione completata. Puoi chiudere questo dialogo e continuare a usare YTSage.",
"installing": "Installazione di FFmpeg... Attendere",
"install_success": "FFmpeg è stato installato con successo!",
"installation_complete_close": "Installazione completata. Ora puoi chiudere questo dialogo e continuare a usare YTSage.",
"try_manual": "Prova invece a usare la guida di installazione manuale."
},
"ytdlp_setup": {
"required_title": "Configurazione di yt-dlp richiesta",
"description": "YTSage richiede yt-dlp per scaricare video.<br><br>yt-dlp non è stato trovato nella directory locale dell'app. YTSage deve configurare yt-dlp per il tuo sistema {os_name}.<br><br>Scegli un'opzione qui sotto:",
"option_auto": "Scarica automaticamente (consigliato)",
"option_manual": "Seleziona il percorso manualmente",
"setup_button": "Configura yt-dlp",
"downloading": "Download di yt-dlp...",
"success": "yt-dlp è stato installato correttamente!",
"error": "Errore: {error}",
"download_failed_title": "Download non riuscito",
"download_failed_message": "Impossibile scaricare yt-dlp: {error}",
"select_executable_title": "Seleziona l'eseguibile yt-dlp",
"copied_to": "yt-dlp copiato correttamente in {path}",
"setup_error_title": "Errore di configurazione",
"copy_error": "Errore durante la copia di yt-dlp nella directory dell'app: {error}",
"invalid_executable_title": "Eseguibile non valido",
"invalid_executable_message": "Il file selezionato non sembra essere un eseguibile yt-dlp valido.",
"verify_error": "Errore durante la verifica dell'eseguibile yt-dlp: {error}",
"setup_failed_title": "Configurazione non riuscita",
"setup_failed_message": "Impossibile configurare yt-dlp. Alcune funzionalità potrebbero non funzionare correttamente.",
"success_dialog_title": "Configurazione di yt-dlp",
"success_dialog_message": "yt-dlp è stato configurato correttamente in:\n{path}",
"file_filter_windows": "File eseguibili (*.exe)",
"file_filter_all": "Tutti i file (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Controllo Versione FFmpeg", "title": "Controllo Versione FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "音声のみ" "audio_only_resolution": "音声のみ"
}, },
"buttons": { "buttons": {
"reset": "リセット",
"download": "ダウンロード", "download": "ダウンロード",
"pause": "一時停止", "pause": "一時停止",
"resume": "再開", "resume": "再開",
@@ -93,7 +94,8 @@
"select_subtitles": "字幕を選択", "select_subtitles": "字幕を選択",
"filter_languages_placeholder": "言語でフィルタ (例: ja, en)...", "filter_languages_placeholder": "言語でフィルタ (例: ja, en)...",
"no_subtitles_available": "利用可能な字幕がありません", "no_subtitles_available": "利用可能な字幕がありません",
"matching": "一致" "matching": "一致",
"ytdlp_log_title": "yt-dlpログ"
}, },
"tabs": { "tabs": {
"cookies": "Cookieでログイン", "cookies": "Cookieでログイン",
@@ -124,7 +126,10 @@
"browser_selected_title": "ブラウザCookieが適用されました", "browser_selected_title": "ブラウザCookieが適用されました",
"browser_applied_message": "ブラウザCookieが抽出されます: {browser}", "browser_applied_message": "ブラウザCookieが抽出されます: {browser}",
"cleared_title": "Cookieがクリアされました", "cleared_title": "Cookieがクリアされました",
"cleared_message": "Cookie設定がクリアされました" "cleared_message": "Cookie設定がクリアされました",
"active_browser": "✓ 有効: ブラウザーのCookie ({browser})",
"active_file": "✓ 有効: Cookieファイル ({file})",
"none_active": "○ 有効なCookieはありません"
}, },
"custom_command": { "custom_command": {
"help_text": "以下にカスタムyt-dlpコマンドを入力してください。現在のURLは自動的に追加されます。<br><br>オプションの完全なリストと使用例については、<a href=\"{docs_url}\">こちらをクリックしてyt-dlp公式ドキュメントを参照してください</a>。<br><br>注: ダウンロードパスとファイル名テンプレートは自動的に処理されます。", "help_text": "以下にカスタムyt-dlpコマンドを入力してください。現在のURLは自動的に追加されます。<br><br>オプションの完全なリストと使用例については、<a href=\"{docs_url}\">こちらをクリックしてyt-dlp公式ドキュメントを参照してください</a>。<br><br>注: ダウンロードパスとファイル名テンプレートは自動的に処理されます。",
@@ -137,7 +142,14 @@
"full_command": "🔧 完全なコマンド: {command}", "full_command": "🔧 完全なコマンド: {command}",
"command_success": "✅ カスタムコマンドが正常に実行されました!", "command_success": "✅ カスタムコマンドが正常に実行されました!",
"command_failed": "❌ コマンドが終了コード{code}で失敗しました", "command_failed": "❌ コマンドが終了コード{code}で失敗しました",
"command_error": "❌ カスタムコマンドの実行エラー: {error}" "command_error": "❌ カスタムコマンドの実行エラー: {error}",
"error_no_url": "❌ エラー: URL が指定されていません。メインウィンドウで URL を入力してください。",
"error_no_command": "❌ エラー: コマンドが指定されていません。yt-dlp の引数を入力してください。",
"executing": "🚀 カスタム yt-dlp コマンドを実行中",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ 引数: {command}",
"download_path_label": "📁 ダウンロード先: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "ダウンロード用のプロキシ設定を構成します。直接接続の場合は空白のままにしてください。", "help_text": "ダウンロード用のプロキシ設定を構成します。直接接続の場合は空白のままにしてください。",
@@ -159,7 +171,15 @@
"invalid_main_url": "メインプロキシのURL形式が無効です", "invalid_main_url": "メインプロキシのURL形式が無効です",
"invalid_geo_url": "地域プロキシのURL形式が無効です", "invalid_geo_url": "地域プロキシのURL形式が無効です",
"main_configured": "メインプロキシが設定されました", "main_configured": "メインプロキシが設定されました",
"geo_configured": "地域プロキシが設定されました" "geo_configured": "地域プロキシが設定されました",
"set_title": "プロキシを設定しました",
"set_message": "メインプロキシを設定して保存しました: {proxy}",
"geo_set_title": "ジオプロキシを設定しました",
"geo_set_message": "ジオ検証プロキシを設定して保存しました: {proxy}",
"cleared_title": "プロキシ設定をクリアしました",
"cleared_message": "すべてのプロキシ設定をクリアして保存しました。",
"saved_main": "保存されたメインプロキシ: {proxy}",
"saved_geo": "保存されたジオプロキシ: {proxy}"
}, },
"download": { "download": {
"preparing": "ダウンロードを準備中...", "preparing": "ダウンロードを準備中...",
@@ -225,6 +245,8 @@
"system_info": "システム情報", "system_info": "システム情報",
"loading": "🔄 システム情報を読み込み中...", "loading": "🔄 システム情報を読み込み中...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 ログ",
"logs_tooltip": "アプリケーションのログフォルダを開く",
"refreshing": "🔄 更新中...", "refreshing": "🔄 更新中...",
"refresh_failed": "更新に失敗しました", "refresh_failed": "更新に失敗しました",
"refresh_failed_message": "バージョン情報を更新できませんでした。", "refresh_failed_message": "バージョン情報を更新できませんでした。",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ {channel}チャンネルへの切り替えに成功しました!", "ytdlp_channel_switched": "✅ {channel}チャンネルへの切り替えに成功しました!",
"ytdlp_channel_switch_failed": "❌ チャンネルの切り替えに失敗しました: {error}", "ytdlp_channel_switch_failed": "❌ チャンネルの切り替えに失敗しました: {error}",
"ytdlp_current_channel": "現在のチャンネル: {channel}", "ytdlp_current_channel": "現在のチャンネル: {channel}",
"app_updates_title": "YTSageの更新",
"check_beta_updates": "ベータ版の更新を受け取る",
"auto_update_title": "自動更新設定", "auto_update_title": "自動更新設定",
"auto_update_header": "🔄 自動更新設定", "auto_update_header": "🔄 自動更新設定",
"auto_update_description": "yt-dlpの自動更新を設定して、最新の機能とバグ修正を確実に入手してください。", "auto_update_description": "yt-dlpの自動更新を設定して、最新の機能とバグ修正を確実に入手してください。",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV(非圧縮)", "audio_format_wav": "WAV(非圧縮)",
"audio_format_opus": "Opus(効率的)", "audio_format_opus": "Opus(効率的)",
"audio_format_m4a": "M4AApple", "audio_format_m4a": "M4AApple",
"audio_format_vorbis": "Vorbis(オープン)" "audio_format_vorbis": "Vorbis(オープン)",
"filename_format": "出力ファイル名の形式",
"filename_format_help": "利用可能な変数: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s。標準のyt-dlp出力テンプレート構文がサポートされています。"
}, },
"main_ui": { "main_ui": {
"url_placeholder": "YouTubeの動画URLまたはプレイリストURLを入力", "url_placeholder": "YouTubeの動画URLまたはプレイリストURLを入力",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "ブラウザCookieが抽出されます: {browser}", "browser_cookies_selected_message": "ブラウザCookieが抽出されます: {browser}",
"error_no_format_info": "エラー: 利用可能なフォーマット情報がありません。", "error_no_format_info": "エラー: 利用可能なフォーマット情報がありません。",
"error_extract_info": "エラー: 基本的な動画情報を抽出できませんでした。リンクを確認してください。", "error_extract_info": "エラー: 基本的な動画情報を抽出できませんでした。リンクを確認してください。",
"analyzing_preparing": "解析中 (0%)... リクエストを準備中", "analyzing_preparing": "リクエストを準備中...",
"analyzing_extracting_basic": "解析中 (15%)... 基本情報を抽出中", "analyzing_extracting_basic": "基本情報を抽出中...",
"analyzing_extracting_detailed": "解析中 (30%)... 詳細情報を抽出中", "analyzing_extracting_detailed": "詳細情報を抽出中...",
"analyzing_processing_video": "解析中 (45%)... 動画データを処理中", "analyzing_processing_video": "動画データを処理中...",
"analyzing_processing_formats": "解析中 (60%)... フォーマットを処理中", "analyzing_processing_formats": "フォーマットを処理中...",
"analyzing_loading_thumbnail": "解析中 (75%)... サムネイルを読み込み中", "analyzing_loading_thumbnail": "サムネイルを読み込み中...",
"analyzing_processing_subtitles": "解析中 (85%)... 字幕を処理中", "analyzing_processing_subtitles": "字幕を処理中...",
"analyzing_updating_table": "解析中 (95%)... フォーマットテーブルを更新中", "analyzing_updating_table": "フォーマットテーブルを更新中...",
"analysis_complete": "解析完了!", "analysis_complete": "解析完了!",
"analyzing_extracting_ytdlp": "解析中 (30%)... 情報を抽出中", "analyzing_extracting_ytdlp": "情報を抽出中...",
"analyzing_processing_data": "解析中 (60%)... データを処理中", "analyzing_fetching_first_video": "最初のビデオの形式を取得しています...",
"analyzing_processing_formats_ytdlp": "解析中 (75%)... フォーマットを処理中", "analyzing_processing_data": "データを処理中...",
"analyzing_loading_thumbnail_ytdlp": "解析中 (85%)... サムネイルを読み込み中", "analyzing_processing_formats_ytdlp": "フォーマットを処理中...",
"analyzing_processing_subtitles_ytdlp": "解析中 (90%)... 字幕を処理中", "analyzing_loading_thumbnail_ytdlp": "サムネイルを読み込み中...",
"analyzing_processing_subtitles_ytdlp": "字幕を処理中...",
"select_subtitles": "字幕を選択...", "select_subtitles": "字幕を選択...",
"sponsorblock_categories": "SponsorBlockカテゴリ...", "sponsorblock_categories": "SponsorBlockカテゴリ...",
"invalid_url_or_enter": "無効なURLまたはURLを入力してください。", "invalid_url_or_enter": "無効なURLまたはURLを入力してください。",
"zero_selected": "0個選択", "zero_selected": "0個選択",
"analyze_first_tooltip": "最初に動画を分析してください", "analyze_first_tooltip": "最初に動画を分析してください",
"audio_mode_disabled": "音声のみモードでは利用できません", "audio_mode_disabled": "音声のみモードでは利用できません",
"select_subtitles_first": "最初に字幕を選択してください" "select_subtitles_first": "最初に字幕を選択してください",
"settings_tooltip": "現在のパス: {path}\n速度制限: {speed_limit}",
"speed_limit_none": "なし",
"open_folder_error": "フォルダーを開けませんでした: {error}",
"time_range_set": "セクション設定: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "スポンサー", "sponsor": "スポンサー",
@@ -408,7 +439,10 @@
"ytdlp_failed": "エラー: yt-dlpが失敗しました: {error}", "ytdlp_failed": "エラー: yt-dlpが失敗しました: {error}",
"parse_failed": "エラー: yt-dlp出力の解析に失敗しました: {error}", "parse_failed": "エラー: yt-dlp出力の解析に失敗しました: {error}",
"analysis_failed": "エラー: 解析が失敗しました: {error}", "analysis_failed": "エラー: 解析が失敗しました: {error}",
"generic_error": "エラー: {error}" "generic_error": "エラー: {error}",
"download_failed_return_code_conflict": "ダウンロードは戻りコード {return_code} で失敗しました。複数の yt-dlp インストールの競合が原因の可能性があります。システムにインストールされた yt-dlp(例: snap や apt)をアンインストールしてアプリを再起動してください。",
"download_failed_return_code": "ダウンロードは戻りコード {return_code} で失敗しました",
"direct_command_error": "直接コマンドのエラー: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "アップデートが利用可能です", "title": "アップデートが利用可能です",
@@ -442,7 +476,8 @@
"next_check": "次回確認: {time}", "next_check": "次回確認: {time}",
"next_check_error": "次回確認: 計算エラー", "next_check_error": "次回確認: 計算エラー",
"checking": "🔄 確認中...", "checking": "🔄 確認中...",
"check_now": "🔍 今すぐ更新を確認" "check_now": "🔍 今すぐ更新を確認",
"current_version": "現在のyt-dlpバージョン: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URLを空にすることはできません", "empty_url": "URLを空にすることはできません",
@@ -473,6 +508,7 @@
"clear_confirm_message": "本当によろしいですか?この操作は取り消せません。", "clear_confirm_message": "本当によろしいですか?この操作は取り消せません。",
"no_history": "まだ履歴がありません", "no_history": "まだ履歴がありません",
"no_history_description": "ダウンロードがここに表示されます", "no_history_description": "ダウンロードがここに表示されます",
"loading": "履歴を読み込み中...",
"search_placeholder": "検索...", "search_placeholder": "検索...",
"open_location": "場所を開く", "open_location": "場所を開く",
"redownload": "再ダウンロード", "redownload": "再ダウンロード",
@@ -491,7 +527,47 @@
"one_entry": "1 件", "one_entry": "1 件",
"redownload_confirm_title": "再ダウンロード?", "redownload_confirm_title": "再ダウンロード?",
"redownload_confirm_message": "再ダウンロードしますか?\n\n{title}", "redownload_confirm_message": "再ダウンロードしますか?\n\n{title}",
"redownload_started": "ダウンロード開始" "redownload_started": "ダウンロード開始",
"no_url_error": "履歴エントリにURLがありません",
"redownload_failed": "再ダウンロードの開始に失敗しました: {error}"
},
"ffmpeg": {
"installation_title": "FFmpegのインストール",
"installation_message": "YTSageは動画処理にFFmpegが必要です。\n\n以下のインストールオプションを選択してください:",
"install_button": "FFmpegをインストール",
"manual_guide": "手動ガイド",
"installation_failed": "FFmpegのインストール中に問題が発生しました。",
"already_installed": "FFmpegはすでにインストールされています!",
"installation_complete": "インストール完了。このダイアログを閉じてYTSageを続けて使用できます。",
"installing": "FFmpegをインストール中... お待ちください",
"install_success": "FFmpegが正常にインストールされました!",
"installation_complete_close": "インストール完了。今すぐこのダイアログを閉じてYTSageを続けて使用できます。",
"try_manual": "代わりに手動インストールガイドをお試しください。"
},
"ytdlp_setup": {
"required_title": "yt-dlpのセットアップが必要です",
"description": "YTSageは動画のダウンロードにyt-dlpが必要です。<br><br>アプリのローカルディレクトリにyt-dlpが見つかりませんでした。{os_name}システム用にyt-dlpをセットアップする必要があります。<br><br>以下のオプションを選択してください:",
"option_auto": "自動でダウンロード(推奨)",
"option_manual": "パスを手動で選択",
"setup_button": "yt-dlpをセットアップ",
"downloading": "yt-dlpをダウンロード中...",
"success": "yt-dlpが正常にインストールされました!",
"error": "エラー: {error}",
"download_failed_title": "ダウンロード失敗",
"download_failed_message": "yt-dlpのダウンロードに失敗しました: {error}",
"select_executable_title": "yt-dlp実行ファイルを選択",
"copied_to": "yt-dlpを{path}にコピーしました",
"setup_error_title": "セットアップエラー",
"copy_error": "yt-dlpをアプリディレクトリにコピーできません: {error}",
"invalid_executable_title": "無効な実行ファイル",
"invalid_executable_message": "選択されたファイルは有効なyt-dlp実行ファイルではありません。",
"verify_error": "yt-dlp実行ファイルの検証に失敗しました: {error}",
"setup_failed_title": "セットアップ失敗",
"setup_failed_message": "yt-dlpのセットアップに失敗しました。いくつかの機能が正しく動作しない可能性があります。",
"success_dialog_title": "yt-dlpセットアップ",
"success_dialog_message": "yt-dlpの設定に成功しました:\n{path}",
"file_filter_windows": "実行ファイル (*.exe)",
"file_filter_all": "すべてのファイル (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "FFmpegバージョンチェッカー", "title": "FFmpegバージョンチェッカー",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Tylko dźwięk" "audio_only_resolution": "Tylko dźwięk"
}, },
"buttons": { "buttons": {
"reset": "Zresetuj",
"download": "Pobierz", "download": "Pobierz",
"pause": "Wstrzymaj", "pause": "Wstrzymaj",
"resume": "Wznów", "resume": "Wznów",
@@ -93,7 +94,8 @@
"select_subtitles": "Wybierz napisy", "select_subtitles": "Wybierz napisy",
"filter_languages_placeholder": "Filtruj języki (np: pl, en)...", "filter_languages_placeholder": "Filtruj języki (np: pl, en)...",
"no_subtitles_available": "Brak dostępnych napisów", "no_subtitles_available": "Brak dostępnych napisów",
"matching": "dopasowujące" "matching": "dopasowujące",
"ytdlp_log_title": "Log yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Zaloguj za pomocą ciasteczek", "cookies": "Zaloguj za pomocą ciasteczek",
@@ -124,7 +126,10 @@
"browser_selected_title": "Zastosowano ciasteczka przeglądarki", "browser_selected_title": "Zastosowano ciasteczka przeglądarki",
"browser_applied_message": "Ciasteczka przeglądarki zostaną wyodrębnione z: {browser}", "browser_applied_message": "Ciasteczka przeglądarki zostaną wyodrębnione z: {browser}",
"cleared_title": "Wyczyszczono ciasteczka", "cleared_title": "Wyczyszczono ciasteczka",
"cleared_message": "Ustawienia ciasteczek zostały wyczyszczone" "cleared_message": "Ustawienia ciasteczek zostały wyczyszczone",
"active_browser": "✓ Aktywne: ciasteczka przeglądarki ({browser})",
"active_file": "✓ Aktywny: plik ciasteczek ({file})",
"none_active": "○ Brak aktywnych ciasteczek"
}, },
"custom_command": { "custom_command": {
"help_text": "Wprowadź niestandardowe polecenie yt-dlp poniżej. Aktualny URL zostanie automatycznie dodany.<br><br>Aby uzyskać pełną listę opcji i przykładów użycia <a href=\"{docs_url}\">kliknij tutaj, aby zobaczyć oficjalną dokumentację yt-dlp</a>.<br><br>Uwaga: Ścieżka pobierania i szablon nazwy pliku są obsługiwane automatycznie.", "help_text": "Wprowadź niestandardowe polecenie yt-dlp poniżej. Aktualny URL zostanie automatycznie dodany.<br><br>Aby uzyskać pełną listę opcji i przykładów użycia <a href=\"{docs_url}\">kliknij tutaj, aby zobaczyć oficjalną dokumentację yt-dlp</a>.<br><br>Uwaga: Ścieżka pobierania i szablon nazwy pliku są obsługiwane automatycznie.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Pełne polecenie: {command}", "full_command": "🔧 Pełne polecenie: {command}",
"command_success": "✅ Polecenie niestandardowe wykonane pomyślnie!", "command_success": "✅ Polecenie niestandardowe wykonane pomyślnie!",
"command_failed": "❌ Polecenie nie powiodło się z kodem wyjścia {code}", "command_failed": "❌ Polecenie nie powiodło się z kodem wyjścia {code}",
"command_error": "❌ Błąd wykonywania polecenia niestandardowego: {error}" "command_error": "❌ Błąd wykonywania polecenia niestandardowego: {error}",
"error_no_url": "❌ Błąd: nie podano URL. Wprowadź URL w głównym oknie.",
"error_no_command": "❌ Błąd: nie podano polecenia. Wprowadź argumenty yt-dlp.",
"executing": "🚀 Uruchamianie niestandardowego polecenia yt-dlp",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argumenty: {command}",
"download_path_label": "📁 Ścieżka pobierania: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Skonfiguruj ustawienia proxy dla pobierania. Pozostaw puste dla bezpośredniego połączenia.", "help_text": "Skonfiguruj ustawienia proxy dla pobierania. Pozostaw puste dla bezpośredniego połączenia.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Nieprawidłowy format URL głównego proxy", "invalid_main_url": "Nieprawidłowy format URL głównego proxy",
"invalid_geo_url": "Nieprawidłowy format URL proxy geograficznego", "invalid_geo_url": "Nieprawidłowy format URL proxy geograficznego",
"main_configured": "Główny proxy skonfigurowany", "main_configured": "Główny proxy skonfigurowany",
"geo_configured": "Proxy geograficzny skonfigurowany" "geo_configured": "Proxy geograficzny skonfigurowany",
"set_title": "Ustawiono proxy",
"set_message": "Główne proxy ustawione i zapisane: {proxy}",
"geo_set_title": "Ustawiono proxy geo",
"geo_set_message": "Proxy weryfikacji geo ustawione i zapisane: {proxy}",
"cleared_title": "Wyczyszczono ustawienia proxy",
"cleared_message": "Wszystkie ustawienia proxy zostały wyczyszczone i zapisane.",
"saved_main": "Zapisany główny proxy: {proxy}",
"saved_geo": "Zapisany proxy geo: {proxy}"
}, },
"download": { "download": {
"preparing": "Przygotowywanie pobierania...", "preparing": "Przygotowywanie pobierania...",
@@ -225,6 +245,8 @@
"system_info": "Informacje systemowe", "system_info": "Informacje systemowe",
"loading": "🔄 Ładowanie informacji systemowych...", "loading": "🔄 Ładowanie informacji systemowych...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Logi",
"logs_tooltip": "Otwórz folder logów aplikacji",
"refreshing": "🔄 Odświeżanie...", "refreshing": "🔄 Odświeżanie...",
"refresh_failed": "Odświeżanie nie powiodło się", "refresh_failed": "Odświeżanie nie powiodło się",
"refresh_failed_message": "Nie można odświeżyć informacji o wersji.", "refresh_failed_message": "Nie można odświeżyć informacji o wersji.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Pomyślnie przełączono na kanał {channel}!", "ytdlp_channel_switched": "✅ Pomyślnie przełączono na kanał {channel}!",
"ytdlp_channel_switch_failed": "❌ Nie udało się przełączyć kanału: {error}", "ytdlp_channel_switch_failed": "❌ Nie udało się przełączyć kanału: {error}",
"ytdlp_current_channel": "Aktualny kanał: {channel}", "ytdlp_current_channel": "Aktualny kanał: {channel}",
"app_updates_title": "Aktualizacje YTSage",
"check_beta_updates": "Otrzymuj aktualizacje beta",
"auto_update_title": "Ustawienia automatycznych aktualizacji", "auto_update_title": "Ustawienia automatycznych aktualizacji",
"auto_update_header": "🔄 Ustawienia automatycznych aktualizacji", "auto_update_header": "🔄 Ustawienia automatycznych aktualizacji",
"auto_update_description": "Skonfiguruj automatyczne aktualizacje dla yt-dlp, aby zapewnić najnowsze funkcje i poprawki błędów.", "auto_update_description": "Skonfiguruj automatyczne aktualizacje dla yt-dlp, aby zapewnić najnowsze funkcje i poprawki błędów.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Nieskompresowany)", "audio_format_wav": "WAV (Nieskompresowany)",
"audio_format_opus": "Opus (Wydajny)", "audio_format_opus": "Opus (Wydajny)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Otwarty)" "audio_format_vorbis": "Vorbis (Otwarty)",
"filename_format": "Format nazwy pliku",
"filename_format_help": "Dostępne zmienne: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Obsługiwana jest standardowa składnia szablonu wyjściowego yt-dlp."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Wprowadź URL wideo YouTube lub playlisty", "url_placeholder": "Wprowadź URL wideo YouTube lub playlisty",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "Ciasteczka przeglądarki zostaną wyodrębnione z: {browser}", "browser_cookies_selected_message": "Ciasteczka przeglądarki zostaną wyodrębnione z: {browser}",
"error_no_format_info": "Błąd: Brak dostępnych informacji o formacie.", "error_no_format_info": "Błąd: Brak dostępnych informacji o formacie.",
"error_extract_info": "Błąd: Nie można wyodrębnić podstawowych informacji o wideo. Sprawdź swój link.", "error_extract_info": "Błąd: Nie można wyodrębnić podstawowych informacji o wideo. Sprawdź swój link.",
"analyzing_preparing": "Analizowanie (0%)... Przygotowywanie żądania", "analyzing_preparing": "Przygotowywanie żądania...",
"analyzing_extracting_basic": "Analizowanie (15%)... Wyodrębnianie podstawowych informacji", "analyzing_extracting_basic": "Wyodrębnianie podstawowych informacji...",
"analyzing_extracting_detailed": "Analizowanie (30%)... Wyodrębnianie szczegółowych informacji", "analyzing_extracting_detailed": "Wyodrębnianie szczegółowych informacji...",
"analyzing_processing_video": "Analizowanie (45%)... Przetwarzanie danych wideo", "analyzing_processing_video": "Przetwarzanie danych wideo...",
"analyzing_processing_formats": "Analizowanie (60%)... Przetwarzanie formatów", "analyzing_processing_formats": "Przetwarzanie formatów...",
"analyzing_loading_thumbnail": "Analizowanie (75%)... Ładowanie miniatury", "analyzing_loading_thumbnail": "Ładowanie miniatury...",
"analyzing_processing_subtitles": "Analizowanie (85%)... Przetwarzanie napisów", "analyzing_processing_subtitles": "Przetwarzanie napisów...",
"analyzing_updating_table": "Analizowanie (95%)... Aktualizowanie tabeli formatów", "analyzing_updating_table": "Aktualizowanie tabeli formatów...",
"analysis_complete": "Analiza zakończona!", "analysis_complete": "Analiza zakończona!",
"analyzing_extracting_ytdlp": "Analizowanie (30%)... Wyodrębnianie informacji", "analyzing_fetching_first_video": "Pobieranie formatów dla pierwszego filmu...",
"analyzing_processing_data": "Analizowanie (60%)... Przetwarzanie danych", "analyzing_extracting_ytdlp": "Wyodrębnianie informacji...",
"analyzing_processing_formats_ytdlp": "Analizowanie (75%)... Przetwarzanie formatów", "analyzing_processing_data": "Przetwarzanie danych...",
"analyzing_loading_thumbnail_ytdlp": "Analizowanie (85%)... Ładowanie miniatury", "analyzing_processing_formats_ytdlp": "Przetwarzanie formatów...",
"analyzing_processing_subtitles_ytdlp": "Analizowanie (90%)... Przetwarzanie napisów", "analyzing_loading_thumbnail_ytdlp": "Ładowanie miniatury...",
"analyzing_processing_subtitles_ytdlp": "Przetwarzanie napisów...",
"select_subtitles": "Wybierz napisy...", "select_subtitles": "Wybierz napisy...",
"sponsorblock_categories": "Kategorie SponsorBlock...", "sponsorblock_categories": "Kategorie SponsorBlock...",
"invalid_url_or_enter": "Nieprawidłowy URL lub wprowadź URL.", "invalid_url_or_enter": "Nieprawidłowy URL lub wprowadź URL.",
"zero_selected": "0 wybranych", "zero_selected": "0 wybranych",
"analyze_first_tooltip": "Najpierw przeanalizuj wideo", "analyze_first_tooltip": "Najpierw przeanalizuj wideo",
"audio_mode_disabled": "Niedostępne w trybie tylko audio", "audio_mode_disabled": "Niedostępne w trybie tylko audio",
"select_subtitles_first": "Najpierw wybierz napisy" "select_subtitles_first": "Najpierw wybierz napisy",
"settings_tooltip": "Bieżąca ścieżka: {path}\nLimit prędkości: {speed_limit}",
"speed_limit_none": "Brak",
"open_folder_error": "Nie można otworzyć folderu: {error}",
"time_range_set": "Ustawiono sekcję: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Błąd: yt-dlp nie powiodło się: {error}", "ytdlp_failed": "Błąd: yt-dlp nie powiodło się: {error}",
"parse_failed": "Błąd: Nie udało się przeanalizować wyjścia yt-dlp: {error}", "parse_failed": "Błąd: Nie udało się przeanalizować wyjścia yt-dlp: {error}",
"analysis_failed": "Błąd: Analiza nie powiodła się: {error}", "analysis_failed": "Błąd: Analiza nie powiodła się: {error}",
"generic_error": "Błąd: {error}" "generic_error": "Błąd: {error}",
"download_failed_return_code_conflict": "Pobieranie nie powiodło się z kodem {return_code}. Może to wynikać z konfliktu wielu instalacji yt-dlp. Spróbuj odinstalować systemowo zainstalowane yt-dlp (np. przez snap lub apt) i uruchom aplikację ponownie.",
"download_failed_return_code": "Pobieranie nie powiodło się z kodem {return_code}",
"direct_command_error": "Błąd w bezpośrednim poleceniu: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Dostępna aktualizacja", "title": "Dostępna aktualizacja",
@@ -442,7 +476,8 @@
"next_check": "Następne sprawdzenie: {time}", "next_check": "Następne sprawdzenie: {time}",
"next_check_error": "Następne sprawdzenie: Błąd obliczania", "next_check_error": "Następne sprawdzenie: Błąd obliczania",
"checking": "🔄 Sprawdzanie...", "checking": "🔄 Sprawdzanie...",
"check_now": "🔍 Sprawdź aktualizacje teraz" "check_now": "🔍 Sprawdź aktualizacje teraz",
"current_version": "Bieżąca wersja yt-dlp: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL nie może być pusty", "empty_url": "URL nie może być pusty",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Czy jesteś pewien? Nie można tego cofnąć.", "clear_confirm_message": "Czy jesteś pewien? Nie można tego cofnąć.",
"no_history": "Brak historii", "no_history": "Brak historii",
"no_history_description": "Twoje pobrane pliki pojawią się tutaj", "no_history_description": "Twoje pobrane pliki pojawią się tutaj",
"loading": "Ładowanie historii...",
"search_placeholder": "Szukaj...", "search_placeholder": "Szukaj...",
"open_location": "Otwórz Lokalizację", "open_location": "Otwórz Lokalizację",
"redownload": "Pobierz Ponownie", "redownload": "Pobierz Ponownie",
@@ -491,7 +527,47 @@
"one_entry": "1 pobranie", "one_entry": "1 pobranie",
"redownload_confirm_title": "Pobrać Ponownie?", "redownload_confirm_title": "Pobrać Ponownie?",
"redownload_confirm_message": "Pobrać ponownie?\n\n{title}", "redownload_confirm_message": "Pobrać ponownie?\n\n{title}",
"redownload_started": "Rozpoczęto pobieranie" "redownload_started": "Rozpoczęto pobieranie",
"no_url_error": "Nie znaleziono URL w wpisie historii",
"redownload_failed": "Nie udało się rozpocząć ponownego pobierania: {error}"
},
"ffmpeg": {
"installation_title": "Instalacja FFmpeg",
"installation_message": "YTSage wymaga FFmpeg do przetwarzania wideo.\n\nWybierz opcję instalacji poniżej:",
"install_button": "Zainstaluj FFmpeg",
"manual_guide": "Instrukcja ręczna",
"installation_failed": "Instalacja FFmpeg napotkała problem.",
"already_installed": "FFmpeg jest już zainstalowany!",
"installation_complete": "Instalacja zakończona. Możesz zamknąć to okno i kontynuować używanie YTSage.",
"installing": "Instalowanie FFmpeg... Proszę czekać",
"install_success": "FFmpeg został pomyślnie zainstalowany!",
"installation_complete_close": "Instalacja zakończona. Możesz teraz zamknąć to okno i kontynuować używanie YTSage.",
"try_manual": "Spróbuj użyć instrukcji instalacji ręcznej."
},
"ytdlp_setup": {
"required_title": "Wymagana konfiguracja yt-dlp",
"description": "YTSage wymaga yt-dlp do pobierania wideo.<br><br>Nie znaleziono yt-dlp w lokalnym katalogu aplikacji. YTSage musi skonfigurować yt-dlp dla systemu {os_name}.<br><br>Wybierz opcję poniżej:",
"option_auto": "Pobierz automatycznie (zalecane)",
"option_manual": "Wybierz ścieżkę ręcznie",
"setup_button": "Skonfiguruj yt-dlp",
"downloading": "Pobieranie yt-dlp...",
"success": "yt-dlp został pomyślnie zainstalowany!",
"error": "Błąd: {error}",
"download_failed_title": "Pobieranie nieudane",
"download_failed_message": "Nie udało się pobrać yt-dlp: {error}",
"select_executable_title": "Wybierz plik wykonywalny yt-dlp",
"copied_to": "yt-dlp skopiowano pomyślnie do {path}",
"setup_error_title": "Błąd konfiguracji",
"copy_error": "Błąd podczas kopiowania yt-dlp do katalogu aplikacji: {error}",
"invalid_executable_title": "Nieprawidłowy plik wykonywalny",
"invalid_executable_message": "Wybrany plik nie wygląda na prawidłowy plik wykonywalny yt-dlp.",
"verify_error": "Błąd podczas weryfikacji pliku yt-dlp: {error}",
"setup_failed_title": "Konfiguracja nieudana",
"setup_failed_message": "Nie udało się skonfigurować yt-dlp. Niektóre funkcje mogą działać nieprawidłowo.",
"success_dialog_title": "Konfiguracja yt-dlp",
"success_dialog_message": "yt-dlp został pomyślnie skonfigurowany w:\n{path}",
"file_filter_windows": "Pliki wykonywalne (*.exe)",
"file_filter_all": "Wszystkie pliki (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Sprawdzanie Wersji FFmpeg", "title": "Sprawdzanie Wersji FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Apenas áudio" "audio_only_resolution": "Apenas áudio"
}, },
"buttons": { "buttons": {
"reset": "Redefinir",
"download": "Baixar", "download": "Baixar",
"pause": "Pausar", "pause": "Pausar",
"resume": "Retomar", "resume": "Retomar",
@@ -93,7 +94,8 @@
"select_subtitles": "Selecionar Legendas", "select_subtitles": "Selecionar Legendas",
"filter_languages_placeholder": "Filtrar idiomas (ex., en, pt)...", "filter_languages_placeholder": "Filtrar idiomas (ex., en, pt)...",
"no_subtitles_available": "Nenhuma legenda disponível", "no_subtitles_available": "Nenhuma legenda disponível",
"matching": "correspondendo" "matching": "correspondendo",
"ytdlp_log_title": "Log do yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Entrar com Cookies", "cookies": "Entrar com Cookies",
@@ -124,7 +126,10 @@
"browser_selected_title": "Cookies do Navegador Aplicados", "browser_selected_title": "Cookies do Navegador Aplicados",
"browser_applied_message": "Os cookies do navegador serão extraídos de: {browser}", "browser_applied_message": "Os cookies do navegador serão extraídos de: {browser}",
"cleared_title": "Cookies Limpos", "cleared_title": "Cookies Limpos",
"cleared_message": "As configurações de cookies foram limpas" "cleared_message": "As configurações de cookies foram limpas",
"active_browser": "✓ Ativos: cookies do navegador ({browser})",
"active_file": "✓ Ativo: arquivo de cookies ({file})",
"none_active": "○ Nenhum cookie ativo"
}, },
"custom_command": { "custom_command": {
"help_text": "Digite seu comando yt-dlp personalizado abaixo. A URL atual será anexada automaticamente.<br><br>Para a lista completa de opções e exemplos de uso, <a href=\"{docs_url}\">clique aqui para ver a documentação oficial do yt-dlp</a>.<br><br>Nota: O caminho de download e modelo de nome de arquivo serão tratados automaticamente.", "help_text": "Digite seu comando yt-dlp personalizado abaixo. A URL atual será anexada automaticamente.<br><br>Para a lista completa de opções e exemplos de uso, <a href=\"{docs_url}\">clique aqui para ver a documentação oficial do yt-dlp</a>.<br><br>Nota: O caminho de download e modelo de nome de arquivo serão tratados automaticamente.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Comando completo: {command}", "full_command": "🔧 Comando completo: {command}",
"command_success": "✅ Comando personalizado executado com sucesso!", "command_success": "✅ Comando personalizado executado com sucesso!",
"command_failed": "❌ Comando falhou com código de saída {code}", "command_failed": "❌ Comando falhou com código de saída {code}",
"command_error": "❌ Erro ao executar comando personalizado: {error}" "command_error": "❌ Erro ao executar comando personalizado: {error}",
"error_no_url": "❌ Erro: nenhuma URL fornecida. Insira uma URL na janela principal.",
"error_no_command": "❌ Erro: nenhum comando fornecido. Insira os argumentos do yt-dlp.",
"executing": "🚀 Executando comando personalizado do yt-dlp",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argumentos: {command}",
"download_path_label": "📁 Caminho de download: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Configure as definições de proxy para download. Deixe vazio para usar conexão direta.", "help_text": "Configure as definições de proxy para download. Deixe vazio para usar conexão direta.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Formato de URL do proxy principal inválido", "invalid_main_url": "Formato de URL do proxy principal inválido",
"invalid_geo_url": "Formato de URL do proxy geográfico inválido", "invalid_geo_url": "Formato de URL do proxy geográfico inválido",
"main_configured": "Proxy principal configurado", "main_configured": "Proxy principal configurado",
"geo_configured": "Proxy geográfico configurado" "geo_configured": "Proxy geográfico configurado",
"set_title": "Proxy definido",
"set_message": "Proxy principal definido e salvo: {proxy}",
"geo_set_title": "Proxy geo definido",
"geo_set_message": "Proxy de verificação geo definido e salvo: {proxy}",
"cleared_title": "Configurações de proxy limpas",
"cleared_message": "Todas as configurações de proxy foram limpas e salvas.",
"saved_main": "Proxy principal salvo: {proxy}",
"saved_geo": "Proxy geo salvo: {proxy}"
}, },
"download": { "download": {
"preparing": "Preparando download...", "preparing": "Preparando download...",
@@ -225,6 +245,8 @@
"system_info": "Informações do Sistema", "system_info": "Informações do Sistema",
"loading": "🔄 Carregando informações do sistema...", "loading": "🔄 Carregando informações do sistema...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Logs",
"logs_tooltip": "Abrir pasta de logs da aplicação",
"refreshing": "🔄 Atualizando...", "refreshing": "🔄 Atualizando...",
"refresh_failed": "Atualização Falhou", "refresh_failed": "Atualização Falhou",
"refresh_failed_message": "Não foi possível atualizar as informações de versão.", "refresh_failed_message": "Não foi possível atualizar as informações de versão.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Mudado com sucesso para o canal {channel}!", "ytdlp_channel_switched": "✅ Mudado com sucesso para o canal {channel}!",
"ytdlp_channel_switch_failed": "❌ Falha ao trocar de canal: {error}", "ytdlp_channel_switch_failed": "❌ Falha ao trocar de canal: {error}",
"ytdlp_current_channel": "Canal atual: {channel}", "ytdlp_current_channel": "Canal atual: {channel}",
"app_updates_title": "Atualizações do YTSage",
"check_beta_updates": "Receber atualizações beta",
"auto_update_title": "Configurações de Auto-Atualização", "auto_update_title": "Configurações de Auto-Atualização",
"auto_update_header": "🔄 Configurações de Auto-Atualização", "auto_update_header": "🔄 Configurações de Auto-Atualização",
"auto_update_description": "Configure atualizações automáticas para o yt-dlp para garantir que você sempre tenha os recursos mais recentes e correções de bugs.", "auto_update_description": "Configure atualizações automáticas para o yt-dlp para garantir que você sempre tenha os recursos mais recentes e correções de bugs.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Não comprimido)", "audio_format_wav": "WAV (Não comprimido)",
"audio_format_opus": "Opus (Eficiente)", "audio_format_opus": "Opus (Eficiente)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Aberto)" "audio_format_vorbis": "Vorbis (Aberto)",
"filename_format": "Formato de nome de arquivo",
"filename_format_help": "Variáveis disponíveis: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. A sintaxe padrão de modelo de saída do yt-dlp é suportada."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Digite a URL do vídeo ou playlist do YouTube", "url_placeholder": "Digite a URL do vídeo ou playlist do YouTube",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "Os cookies do navegador serão extraídos de: {browser}", "browser_cookies_selected_message": "Os cookies do navegador serão extraídos de: {browser}",
"error_no_format_info": "Erro: Nenhuma informação de formato disponível.", "error_no_format_info": "Erro: Nenhuma informação de formato disponível.",
"error_extract_info": "Erro: Não foi possível extrair informações básicas do vídeo. Verifique seu link.", "error_extract_info": "Erro: Não foi possível extrair informações básicas do vídeo. Verifique seu link.",
"analyzing_preparing": "Analisando (0%)... Preparando solicitação", "analyzing_preparing": "Preparando solicitação...",
"analyzing_extracting_basic": "Analisando (15%)... Extraindo informações básicas", "analyzing_extracting_basic": "Extraindo informações básicas...",
"analyzing_extracting_detailed": "Analisando (30%)... Extraindo informações detalhadas", "analyzing_extracting_detailed": "Extraindo informações detalhadas...",
"analyzing_processing_video": "Analisando (45%)... Processando dados do vídeo", "analyzing_processing_video": "Processando dados do vídeo...",
"analyzing_processing_formats": "Analisando (60%)... Processando formatos", "analyzing_processing_formats": "Processando formatos...",
"analyzing_loading_thumbnail": "Analisando (75%)... Carregando miniatura", "analyzing_loading_thumbnail": "Carregando miniatura...",
"analyzing_processing_subtitles": "Analisando (85%)... Processando legendas", "analyzing_processing_subtitles": "Processando legendas...",
"analyzing_updating_table": "Analisando (95%)... Atualizando tabela de formatos", "analyzing_updating_table": "Atualizando tabela de formatos...",
"analysis_complete": "Análise completa!", "analysis_complete": "Análise completa!",
"analyzing_extracting_ytdlp": "Analisando (30%)... Extraindo informações", "analyzing_fetching_first_video": "Obtendo formatos do primeiro vídeo...",
"analyzing_processing_data": "Analisando (60%)... Processando dados", "analyzing_extracting_ytdlp": "Extraindo informações...",
"analyzing_processing_formats_ytdlp": "Analisando (75%)... Processando formatos", "analyzing_processing_data": "Processando dados...",
"analyzing_loading_thumbnail_ytdlp": "Analisando (85%)... Carregando miniatura", "analyzing_processing_formats_ytdlp": "Processando formatos...",
"analyzing_processing_subtitles_ytdlp": "Analisando (90%)... Processando legendas", "analyzing_loading_thumbnail_ytdlp": "Carregando miniatura...",
"analyzing_processing_subtitles_ytdlp": "Processando legendas...",
"select_subtitles": "Selecionar Legendas...", "select_subtitles": "Selecionar Legendas...",
"sponsorblock_categories": "Categorias SponsorBlock...", "sponsorblock_categories": "Categorias SponsorBlock...",
"invalid_url_or_enter": "URL inválido ou por favor insira um URL.", "invalid_url_or_enter": "URL inválido ou por favor insira um URL.",
"zero_selected": "0 selecionados", "zero_selected": "0 selecionados",
"analyze_first_tooltip": "Por favor analise o vídeo primeiro", "analyze_first_tooltip": "Por favor analise o vídeo primeiro",
"audio_mode_disabled": "Não disponível no modo apenas áudio", "audio_mode_disabled": "Não disponível no modo apenas áudio",
"select_subtitles_first": "Por favor selecione legendas primeiro" "select_subtitles_first": "Por favor selecione legendas primeiro",
"settings_tooltip": "Caminho atual: {path}\nLimite de velocidade: {speed_limit}",
"speed_limit_none": "Nenhum",
"open_folder_error": "Não foi possível abrir a pasta: {error}",
"time_range_set": "Seção definida: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Patrocinador", "sponsor": "Patrocinador",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Erro: yt-dlp falhou: {error}", "ytdlp_failed": "Erro: yt-dlp falhou: {error}",
"parse_failed": "Erro: Falha ao analisar a saída do yt-dlp: {error}", "parse_failed": "Erro: Falha ao analisar a saída do yt-dlp: {error}",
"analysis_failed": "Erro: Análise falhou: {error}", "analysis_failed": "Erro: Análise falhou: {error}",
"generic_error": "Erro: {error}" "generic_error": "Erro: {error}",
"download_failed_return_code_conflict": "O download falhou com o código de retorno {return_code}. Isso pode ser devido a um conflito com várias instalações do yt-dlp. Tente desinstalar qualquer yt-dlp instalado no sistema (ex.: snap ou apt) e reinicie o aplicativo.",
"download_failed_return_code": "O download falhou com o código de retorno {return_code}",
"direct_command_error": "Erro no comando direto: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Atualização Disponível", "title": "Atualização Disponível",
@@ -442,7 +476,8 @@
"next_check": "Próxima verificação: {time}", "next_check": "Próxima verificação: {time}",
"next_check_error": "Próxima verificação: Erro ao calcular", "next_check_error": "Próxima verificação: Erro ao calcular",
"checking": "🔄 Verificando...", "checking": "🔄 Verificando...",
"check_now": "🔍 Verificar atualizações agora" "check_now": "🔍 Verificar atualizações agora",
"current_version": "Versão atual do yt-dlp: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "O URL não pode estar vazio", "empty_url": "O URL não pode estar vazio",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Tem certeza? Isto não pode ser desfeito.", "clear_confirm_message": "Tem certeza? Isto não pode ser desfeito.",
"no_history": "Nenhum histórico ainda", "no_history": "Nenhum histórico ainda",
"no_history_description": "Seus downloads aparecerão aqui", "no_history_description": "Seus downloads aparecerão aqui",
"loading": "Carregando histórico...",
"search_placeholder": "Pesquisar...", "search_placeholder": "Pesquisar...",
"open_location": "Abrir Local", "open_location": "Abrir Local",
"redownload": "Baixar Novamente", "redownload": "Baixar Novamente",
@@ -491,7 +527,47 @@
"one_entry": "1 download", "one_entry": "1 download",
"redownload_confirm_title": "Baixar Novamente?", "redownload_confirm_title": "Baixar Novamente?",
"redownload_confirm_message": "Baixar novamente?\n\n{title}", "redownload_confirm_message": "Baixar novamente?\n\n{title}",
"redownload_started": "Download iniciado" "redownload_started": "Download iniciado",
"no_url_error": "Nenhuma URL encontrada no item do histórico",
"redownload_failed": "Falha ao iniciar o novo download: {error}"
},
"ffmpeg": {
"installation_title": "Instalação do FFmpeg",
"installation_message": "O YTSage precisa do FFmpeg para processar vídeos.\n\nEscolha uma opção de instalação abaixo:",
"install_button": "Instalar FFmpeg",
"manual_guide": "Guia manual",
"installation_failed": "A instalação do FFmpeg encontrou um problema.",
"already_installed": "O FFmpeg já está instalado!",
"installation_complete": "Instalação concluída. Você pode fechar este diálogo e continuar usando o YTSage.",
"installing": "Instalando FFmpeg... Aguarde",
"install_success": "FFmpeg instalado com sucesso!",
"installation_complete_close": "Instalação concluída. Agora você pode fechar este diálogo e continuar usando o YTSage.",
"try_manual": "Tente usar o guia de instalação manual."
},
"ytdlp_setup": {
"required_title": "Configuração do yt-dlp necessária",
"description": "O YTSage precisa do yt-dlp para baixar vídeos.<br><br>O yt-dlp não foi encontrado no diretório local do aplicativo. O YTSage precisa configurar o yt-dlp para o seu sistema {os_name}.<br><br>Escolha uma opção abaixo:",
"option_auto": "Baixar automaticamente (recomendado)",
"option_manual": "Selecionar caminho manualmente",
"setup_button": "Configurar yt-dlp",
"downloading": "Baixando yt-dlp...",
"success": "yt-dlp foi instalado com sucesso!",
"error": "Erro: {error}",
"download_failed_title": "Falha no download",
"download_failed_message": "Falha ao baixar yt-dlp: {error}",
"select_executable_title": "Selecionar executável do yt-dlp",
"copied_to": "yt-dlp copiado com sucesso para {path}",
"setup_error_title": "Erro de configuração",
"copy_error": "Erro ao copiar yt-dlp para o diretório do aplicativo: {error}",
"invalid_executable_title": "Executável inválido",
"invalid_executable_message": "O arquivo selecionado não parece ser um executável válido do yt-dlp.",
"verify_error": "Erro ao verificar o executável do yt-dlp: {error}",
"setup_failed_title": "Configuração falhou",
"setup_failed_message": "Falha ao configurar o yt-dlp. Alguns recursos podem não funcionar corretamente.",
"success_dialog_title": "Configuração do yt-dlp",
"success_dialog_message": "yt-dlp foi configurado com sucesso em:\n{path}",
"file_filter_windows": "Arquivos executáveis (*.exe)",
"file_filter_all": "Todos os arquivos (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Verificador de Versão FFmpeg", "title": "Verificador de Versão FFmpeg",
+98 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Только аудио" "audio_only_resolution": "Только аудио"
}, },
"buttons": { "buttons": {
"reset": "Сброс",
"download": "Скачать", "download": "Скачать",
"pause": "Пауза", "pause": "Пауза",
"resume": "Продолжить", "resume": "Продолжить",
@@ -93,7 +94,8 @@
"select_subtitles": "Выбрать субтитры", "select_subtitles": "Выбрать субтитры",
"filter_languages_placeholder": "Фильтр языков (например, en, ru)...", "filter_languages_placeholder": "Фильтр языков (например, en, ru)...",
"no_subtitles_available": "Субтитры недоступны", "no_subtitles_available": "Субтитры недоступны",
"matching": "соответствующие" "matching": "соответствующие",
"ytdlp_log_title": "Журнал yt-dlp"
}, },
"tabs": { "tabs": {
"cookies": "Войти через Cookie", "cookies": "Войти через Cookie",
@@ -124,7 +126,10 @@
"browser_selected_title": "Cookie браузера применены", "browser_selected_title": "Cookie браузера применены",
"browser_applied_message": "Cookie браузера будут извлечены из: {browser}", "browser_applied_message": "Cookie браузера будут извлечены из: {browser}",
"cleared_title": "Cookie очищены", "cleared_title": "Cookie очищены",
"cleared_message": "Настройки cookie были очищены" "cleared_message": "Настройки cookie были очищены",
"active_browser": "✓ Активны: cookie браузера ({browser})",
"active_file": "✓ Активен: файл cookie ({file})",
"none_active": "○ Нет активных cookie"
}, },
"custom_command": { "custom_command": {
"help_text": "Введите пользовательскую команду yt-dlp ниже. Текущий URL будет добавлен автоматически.<br><br>Для полного списка опций и примеров использования, <a href=\"{docs_url}\">нажмите здесь для просмотра официальной документации yt-dlp</a>.<br><br>Примечание: Путь загрузки и шаблон имени файла будут обработаны автоматически.", "help_text": "Введите пользовательскую команду yt-dlp ниже. Текущий URL будет добавлен автоматически.<br><br>Для полного списка опций и примеров использования, <a href=\"{docs_url}\">нажмите здесь для просмотра официальной документации yt-dlp</a>.<br><br>Примечание: Путь загрузки и шаблон имени файла будут обработаны автоматически.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Полная команда: {command}", "full_command": "🔧 Полная команда: {command}",
"command_success": "✅ Пользовательская команда выполнена успешно!", "command_success": "✅ Пользовательская команда выполнена успешно!",
"command_failed": "❌ Команда завершилась с кодом ошибки {code}", "command_failed": "❌ Команда завершилась с кодом ошибки {code}",
"command_error": "❌ Ошибка выполнения пользовательской команды: {error}" "command_error": "❌ Ошибка выполнения пользовательской команды: {error}",
"error_no_url": "❌ Ошибка: URL не указан. Введите URL в главном окне.",
"error_no_command": "❌ Ошибка: команда не указана. Введите аргументы yt-dlp.",
"executing": "🚀 Выполняется пользовательская команда yt-dlp",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Аргументы: {command}",
"download_path_label": "📁 Путь загрузки: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "Настройте параметры прокси для загрузки. Оставьте пустым для прямого подключения.", "help_text": "Настройте параметры прокси для загрузки. Оставьте пустым для прямого подключения.",
@@ -159,7 +171,15 @@
"invalid_main_url": "Неверный формат URL основного прокси", "invalid_main_url": "Неверный формат URL основного прокси",
"invalid_geo_url": "Неверный формат URL гео-прокси", "invalid_geo_url": "Неверный формат URL гео-прокси",
"main_configured": "Основной прокси настроен", "main_configured": "Основной прокси настроен",
"geo_configured": "Гео-прокси настроен" "geo_configured": "Гео-прокси настроен",
"set_title": "Прокси задан",
"set_message": "Основной прокси задан и сохранён: {proxy}",
"geo_set_title": "Гео‑прокси задан",
"geo_set_message": "Прокси для гео‑проверки задан и сохранён: {proxy}",
"cleared_title": "Настройки прокси очищены",
"cleared_message": "Все настройки прокси очищены и сохранены.",
"saved_main": "Сохранён основной прокси: {proxy}",
"saved_geo": "Сохранён гео‑прокси: {proxy}"
}, },
"download": { "download": {
"preparing": "Подготовка загрузки...", "preparing": "Подготовка загрузки...",
@@ -225,6 +245,8 @@
"system_info": "Системная информация", "system_info": "Системная информация",
"loading": "🔄 Загрузка системной информации...", "loading": "🔄 Загрузка системной информации...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Логи",
"logs_tooltip": "Открыть папку с логами приложения",
"refreshing": "🔄 Обновление...", "refreshing": "🔄 Обновление...",
"refresh_failed": "Обновление не удалось", "refresh_failed": "Обновление не удалось",
"refresh_failed_message": "Не удалось обновить информацию о версии.", "refresh_failed_message": "Не удалось обновить информацию о версии.",
@@ -277,6 +299,8 @@
"ytdlp_channel_switched": "✅ Успешно переключено на канал {channel}!", "ytdlp_channel_switched": "✅ Успешно переключено на канал {channel}!",
"ytdlp_channel_switch_failed": "❌ Не удалось переключить канал: {error}", "ytdlp_channel_switch_failed": "❌ Не удалось переключить канал: {error}",
"ytdlp_current_channel": "Текущий канал: {channel}", "ytdlp_current_channel": "Текущий канал: {channel}",
"app_updates_title": "Обновления YTSage",
"check_beta_updates": "Получать бета-обновления",
"auto_update_title": "Настройки автообновления", "auto_update_title": "Настройки автообновления",
"auto_update_header": "🔄 Настройки автообновления", "auto_update_header": "🔄 Настройки автообновления",
"auto_update_description": "Настройте автоматические обновления для yt-dlp, чтобы всегда иметь последние функции и исправления ошибок.", "auto_update_description": "Настройте автоматические обновления для yt-dlp, чтобы всегда иметь последние функции и исправления ошибок.",
@@ -307,7 +331,9 @@
"audio_format_wav": "WAV (Несжатый)", "audio_format_wav": "WAV (Несжатый)",
"audio_format_opus": "Opus (Эффективный)", "audio_format_opus": "Opus (Эффективный)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Открытый)" "audio_format_vorbis": "Vorbis (Открытый)",
"filename_format": "Формат имени файла",
"filename_format_help": "Доступные переменные: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Поддерживается стандартный синтаксис шаблона вывода yt-dlp."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "Введите URL видео или плейлиста YouTube", "url_placeholder": "Введите URL видео или плейлиста YouTube",
@@ -326,27 +352,32 @@
"browser_cookies_selected_message": "Cookie браузера будут извлечены из: {browser}", "browser_cookies_selected_message": "Cookie браузера будут извлечены из: {browser}",
"error_no_format_info": "Ошибка: Информация о формате недоступна.", "error_no_format_info": "Ошибка: Информация о формате недоступна.",
"error_extract_info": "Ошибка: Не удалось извлечь базовую информацию о видео. Проверьте вашу ссылку.", "error_extract_info": "Ошибка: Не удалось извлечь базовую информацию о видео. Проверьте вашу ссылку.",
"analyzing_preparing": "Анализ (0%)... Подготовка запроса", "analyzing_preparing": "Подготовка запроса...",
"analyzing_extracting_basic": "Анализ (15%)... Извлечение базовой информации", "analyzing_extracting_basic": "Извлечение базовой информации...",
"analyzing_extracting_detailed": "Анализ (30%)... Извлечение подробной информации", "analyzing_extracting_detailed": "Извлечение подробной информации...",
"analyzing_processing_video": "Анализ (45%)... Обработка данных видео", "analyzing_processing_video": "Обработка данных видео...",
"analyzing_processing_formats": "Анализ (60%)... Обработка форматов", "analyzing_processing_formats": "Обработка форматов...",
"analyzing_loading_thumbnail": "Анализ (75%)... Загрузка миниатюры", "analyzing_loading_thumbnail": "Загрузка миниатюры...",
"analyzing_processing_subtitles": "Анализ (85%)... Обработка субтитров", "analyzing_processing_subtitles": "Обработка субтитров...",
"analyzing_updating_table": "Анализ (95%)... Обновление таблицы форматов", "analyzing_updating_table": "Обновление таблицы форматов...",
"analysis_complete": "Анализ завершен!", "analysis_complete": "Анализ завершен!",
"analyzing_extracting_ytdlp": "Анализ (30%)... Извлечение информации", "analyzing_fetching_first_video": "Получение форматов для первого видео...",
"analyzing_processing_data": "Анализ (60%)... Обработка данных", "analyzing_extracting_ytdlp": "Извлечение информации...",
"analyzing_processing_formats_ytdlp": "Анализ (75%)... Обработка форматов", "analyzing_processing_data": "Обработка данных...",
"analyzing_loading_thumbnail_ytdlp": "Анализ (85%)... Загрузка миниатюры", "analyzing_processing_formats_ytdlp": "Обработка форматов...",
"analyzing_processing_subtitles_ytdlp": "Анализ (90%)... Обработка субтитров", "analyzing_loading_thumbnail_ytdlp": "Загрузка миниатюры...",
"analyzing_processing_subtitles_ytdlp": "Обработка субтитров...",
"select_subtitles": "Выбрать субтитры...", "select_subtitles": "Выбрать субтитры...",
"sponsorblock_categories": "Категории SponsorBlock...", "sponsorblock_categories": "Категории SponsorBlock...",
"invalid_url_or_enter": "Неверный URL или пожалуйста введите URL.", "invalid_url_or_enter": "Неверный URL или пожалуйста введите URL.",
"zero_selected": "0 выбрано", "zero_selected": "0 выбрано",
"analyze_first_tooltip": "Пожалуйста, сначала проанализируйте видео", "analyze_first_tooltip": "Пожалуйста, сначала проанализируйте видео",
"audio_mode_disabled": "Недоступно в режиме только аудио", "audio_mode_disabled": "Недоступно в режиме только аудио",
"select_subtitles_first": "Пожалуйста, сначала выберите субтитры" "select_subtitles_first": "Пожалуйста, сначала выберите субтитры",
"settings_tooltip": "Текущий путь: {path}\nЛимит скорости: {speed_limit}",
"speed_limit_none": "Нет",
"open_folder_error": "Не удалось открыть папку: {error}",
"time_range_set": "Установлен участок: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Спонсор", "sponsor": "Спонсор",
@@ -408,7 +439,10 @@
"ytdlp_failed": "Ошибка: yt-dlp завершился с ошибкой: {error}", "ytdlp_failed": "Ошибка: yt-dlp завершился с ошибкой: {error}",
"parse_failed": "Ошибка: Не удалось разобрать вывод yt-dlp: {error}", "parse_failed": "Ошибка: Не удалось разобрать вывод yt-dlp: {error}",
"analysis_failed": "Ошибка: Анализ не удался: {error}", "analysis_failed": "Ошибка: Анализ не удался: {error}",
"generic_error": "Ошибка: {error}" "generic_error": "Ошибка: {error}",
"download_failed_return_code_conflict": "Загрузка завершилась ошибкой с кодом {return_code}. Это может быть из-за конфликта нескольких установок yt-dlp. Попробуйте удалить системно установленный yt-dlp (например, через snap или apt) и перезапустите приложение.",
"download_failed_return_code": "Загрузка не удалась, код {return_code}",
"direct_command_error": "Ошибка в прямой команде: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Доступно обновление", "title": "Доступно обновление",
@@ -442,7 +476,8 @@
"next_check": "Следующая проверка: {time}", "next_check": "Следующая проверка: {time}",
"next_check_error": "Следующая проверка: Ошибка расчета", "next_check_error": "Следующая проверка: Ошибка расчета",
"checking": "🔄 Проверка...", "checking": "🔄 Проверка...",
"check_now": "🔍 Проверить обновления сейчас" "check_now": "🔍 Проверить обновления сейчас",
"current_version": "Текущая версия yt-dlp: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL не может быть пустым", "empty_url": "URL не может быть пустым",
@@ -473,6 +508,7 @@
"clear_confirm_message": "Вы уверены? Это нельзя отменить.", "clear_confirm_message": "Вы уверены? Это нельзя отменить.",
"no_history": "История пока пуста", "no_history": "История пока пуста",
"no_history_description": "Загрузки появятся здесь", "no_history_description": "Загрузки появятся здесь",
"loading": "Загрузка истории...",
"search_placeholder": "Поиск...", "search_placeholder": "Поиск...",
"open_location": "Открыть Папку", "open_location": "Открыть Папку",
"redownload": "Загрузить Снова", "redownload": "Загрузить Снова",
@@ -491,7 +527,47 @@
"one_entry": "1 загрузка", "one_entry": "1 загрузка",
"redownload_confirm_title": "Загрузить Снова?", "redownload_confirm_title": "Загрузить Снова?",
"redownload_confirm_message": "Загрузить снова?\n\n{title}", "redownload_confirm_message": "Загрузить снова?\n\n{title}",
"redownload_started": "Загрузка начата" "redownload_started": "Загрузка начата",
"no_url_error": "В записи истории не найден URL",
"redownload_failed": "Не удалось запустить повторную загрузку: {error}"
},
"ffmpeg": {
"installation_title": "Установка FFmpeg",
"installation_message": "YTSage требуется FFmpeg для обработки видео.\n\nВыберите вариант установки ниже:",
"install_button": "Установить FFmpeg",
"manual_guide": "Руководство вручную",
"installation_failed": "При установке FFmpeg возникла проблема.",
"already_installed": "FFmpeg уже установлен!",
"installation_complete": "Установка завершена. Вы можете закрыть это окно и продолжить использовать YTSage.",
"installing": "Установка FFmpeg... Пожалуйста, подождите",
"install_success": "FFmpeg успешно установлен!",
"installation_complete_close": "Установка завершена. Теперь вы можете закрыть это окно и продолжить использовать YTSage.",
"try_manual": "Пожалуйста, попробуйте воспользоваться руководством по ручной установке."
},
"ytdlp_setup": {
"required_title": "Требуется настройка yt-dlp",
"description": "YTSage требует yt-dlp для загрузки видео.<br><br>yt-dlp не найден в локальном каталоге приложения. YTSage нужно настроить yt-dlp для вашей системы {os_name}.<br><br>Выберите вариант ниже:",
"option_auto": "Скачать автоматически (рекомендуется)",
"option_manual": "Выбрать путь вручную",
"setup_button": "Настроить yt-dlp",
"downloading": "Загрузка yt-dlp...",
"success": "yt-dlp успешно установлен!",
"error": "Ошибка: {error}",
"download_failed_title": "Сбой загрузки",
"download_failed_message": "Не удалось скачать yt-dlp: {error}",
"select_executable_title": "Выберите исполняемый файл yt-dlp",
"copied_to": "yt-dlp успешно скопирован в {path}",
"setup_error_title": "Ошибка настройки",
"copy_error": "Ошибка при копировании yt-dlp в каталог приложения: {error}",
"invalid_executable_title": "Недопустимый исполняемый файл",
"invalid_executable_message": "Выбранный файл не является корректным исполняемым файлом yt-dlp.",
"verify_error": "Ошибка при проверке исполняемого файла yt-dlp: {error}",
"setup_failed_title": "Настройка не удалась",
"setup_failed_message": "Не удалось настроить yt-dlp. Некоторые функции могут работать некорректно.",
"success_dialog_title": "Настройка yt-dlp",
"success_dialog_message": "yt-dlp успешно настроен по пути:\n{path}",
"file_filter_windows": "Исполняемые файлы (*.exe)",
"file_filter_all": "Все файлы (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "Проверка версии FFmpeg", "title": "Проверка версии FFmpeg",
+89 -22
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "Sadece ses" "audio_only_resolution": "Sadece ses"
}, },
"buttons": { "buttons": {
"reset": "Sıfırla",
"download": "İndir", "download": "İndir",
"pause": "Duraklat", "pause": "Duraklat",
"resume": "Devam Et", "resume": "Devam Et",
@@ -93,7 +94,8 @@
"select_subtitles": "Altyazı seç", "select_subtitles": "Altyazı seç",
"filter_languages_placeholder": "Dilleri filtrele (örn: tr, en)...", "filter_languages_placeholder": "Dilleri filtrele (örn: tr, en)...",
"no_subtitles_available": "Altyazı mevcut değil", "no_subtitles_available": "Altyazı mevcut değil",
"matching": "eşleşen" "matching": "eşleşen",
"ytdlp_log_title": "yt-dlp Günlüğü"
}, },
"tabs": { "tabs": {
"cookies": "Çerezlerle giriş yap", "cookies": "Çerezlerle giriş yap",
@@ -124,7 +126,10 @@
"browser_selected_title": "Tarayıcı Çerezleri Uygulandı", "browser_selected_title": "Tarayıcı Çerezleri Uygulandı",
"browser_applied_message": "Tarayıcı çerezleri şuradan çıkarılacak: {browser}", "browser_applied_message": "Tarayıcı çerezleri şuradan çıkarılacak: {browser}",
"cleared_title": "Çerezler Temizlendi", "cleared_title": "Çerezler Temizlendi",
"cleared_message": "Çerez ayarları temizlendi" "cleared_message": "Çerez ayarları temizlendi",
"active_browser": "✓ Etkin: Tarayıcı çerezleri ({browser})",
"active_file": "✓ Etkin: Çerez dosyası ({file})",
"none_active": "○ Etkin çerez yok"
}, },
"custom_command": { "custom_command": {
"help_text": "Özel yt-dlp komutunuzu aşağıya girin. Mevcut URL otomatik olarak eklenecektir.<br><br>Seçeneklerin tam listesi ve kullanım örnekleri için <a href=\"{docs_url}\">resmi yt-dlp belgelerini görmek için buraya tıklayın</a>.<br><br>Not: İndirme yolu ve dosya adı şablonu otomatik olarak işlenir.", "help_text": "Özel yt-dlp komutunuzu aşağıya girin. Mevcut URL otomatik olarak eklenecektir.<br><br>Seçeneklerin tam listesi ve kullanım örnekleri için <a href=\"{docs_url}\">resmi yt-dlp belgelerini görmek için buraya tıklayın</a>.<br><br>Not: İndirme yolu ve dosya adı şablonu otomatik olarak işlenir.",
@@ -137,7 +142,14 @@
"full_command": "🔧 Tam komut: {command}", "full_command": "🔧 Tam komut: {command}",
"command_success": "✅ Özel komut başarıyla çalıştırıldı!", "command_success": "✅ Özel komut başarıyla çalıştırıldı!",
"command_failed": "❌ Komut {code} çıkış koduyla başarısız oldu", "command_failed": "❌ Komut {code} çıkış koduyla başarısız oldu",
"command_error": "❌ Özel komut çalıştırma hatası: {error}" "command_error": "❌ Özel komut çalıştırılırken hata: {error}",
"error_no_url": "❌ Hata: URL sağlanmadı. Lütfen ana pencerede bir URL girin.",
"error_no_command": "❌ Hata: Komut sağlanmadı. Lütfen yt-dlp argümanlarını girin.",
"executing": "🚀 Özel yt-dlp komutu çalıştırılıyor",
"url_label": "📍 URL: {url}",
"args_label": "⚙️ Argümanlar: {command}",
"download_path_label": "📁 İndirme yolu: {path}",
"separator": "=================================================="
}, },
"proxy": { "proxy": {
"help_text": "İndirmeler için proxy ayarlarını yapılandırın. Doğrudan bağlantı için boş bırakın.", "help_text": "İndirmeler için proxy ayarlarını yapılandırın. Doğrudan bağlantı için boş bırakın.",
@@ -159,7 +171,13 @@
"invalid_main_url": "Geçersiz ana proxy URL formatı", "invalid_main_url": "Geçersiz ana proxy URL formatı",
"invalid_geo_url": "Geçersiz coğrafi proxy URL formatı", "invalid_geo_url": "Geçersiz coğrafi proxy URL formatı",
"main_configured": "Ana proxy yapılandırıldı", "main_configured": "Ana proxy yapılandırıldı",
"geo_configured": "Coğrafi proxy yapılandırıldı" "geo_configured": "Coğrafi proxy yapılandırıldı",
"set_title": "Proxy ayarlandı",
"set_message": "Ana proxy ayarlandı ve kaydedildi: {proxy}",
"geo_set_title": "Coğrafi proxy ayarlandı",
"geo_set_message": "Coğrafi doğrulama proxy'si ayarlandı ve kaydedildi: {proxy}",
"cleared_title": "Proxy ayarları temizlendi",
"cleared_message": "Tüm proxy ayarları temizlendi ve kaydedildi."
}, },
"download": { "download": {
"preparing": "İndirme hazırlanıyor...", "preparing": "İndirme hazırlanıyor...",
@@ -225,6 +243,8 @@
"system_info": "Sistem bilgileri", "system_info": "Sistem bilgileri",
"loading": "🔄 Sistem bilgileri yükleniyor...", "loading": "🔄 Sistem bilgileri yükleniyor...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 Kayıtlar",
"logs_tooltip": "Uygulama kayıt klasörünü aç",
"refreshing": "🔄 Yenileniyor...", "refreshing": "🔄 Yenileniyor...",
"refresh_failed": "Yenileme başarısız", "refresh_failed": "Yenileme başarısız",
"refresh_failed_message": "Sürüm bilgileri yenilenemedi.", "refresh_failed_message": "Sürüm bilgileri yenilenemedi.",
@@ -277,6 +297,8 @@
"ytdlp_channel_switched": "✅ Başarıyla {channel} kanalına geçildi!", "ytdlp_channel_switched": "✅ Başarıyla {channel} kanalına geçildi!",
"ytdlp_channel_switch_failed": "❌ Kanal değiştirilemedi: {error}", "ytdlp_channel_switch_failed": "❌ Kanal değiştirilemedi: {error}",
"ytdlp_current_channel": "Mevcut kanal: {channel}", "ytdlp_current_channel": "Mevcut kanal: {channel}",
"app_updates_title": "YTSage Güncellemeleri",
"check_beta_updates": "Beta Güncellemelerini Al",
"auto_update_title": "Otomatik güncelleme ayarları", "auto_update_title": "Otomatik güncelleme ayarları",
"auto_update_header": "🔄 Otomatik güncelleme ayarları", "auto_update_header": "🔄 Otomatik güncelleme ayarları",
"auto_update_description": "En son özelliklere ve hata düzeltmelerine sahip olmak için yt-dlp otomatik güncellemelerini yapılandırın.", "auto_update_description": "En son özelliklere ve hata düzeltmelerine sahip olmak için yt-dlp otomatik güncellemelerini yapılandırın.",
@@ -307,7 +329,9 @@
"audio_format_wav": "WAV (Sıkıştırılmamış)", "audio_format_wav": "WAV (Sıkıştırılmamış)",
"audio_format_opus": "Opus (Verimli)", "audio_format_opus": "Opus (Verimli)",
"audio_format_m4a": "M4A (Apple)", "audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Açık)" "audio_format_vorbis": "Vorbis (Açık)",
"filename_format": "Çıktı Dosya Adı Formatı",
"filename_format_help": "Kullanılabilir değişkenler: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Standart yt-dlp çıktı şablonu sözdizimi desteklenir."
}, },
"main_ui": { "main_ui": {
"url_placeholder": "YouTube video veya oynatma listesi URL'sini girin", "url_placeholder": "YouTube video veya oynatma listesi URL'sini girin",
@@ -326,27 +350,32 @@
"browser_cookies_selected_message": "Tarayıcı çerezleri şuradan çıkarılacak: {browser}", "browser_cookies_selected_message": "Tarayıcı çerezleri şuradan çıkarılacak: {browser}",
"error_no_format_info": "Hata: Format bilgisi mevcut değil.", "error_no_format_info": "Hata: Format bilgisi mevcut değil.",
"error_extract_info": "Hata: Temel video bilgileri çıkarılamadı. Lütfen bağlantınızı kontrol edin.", "error_extract_info": "Hata: Temel video bilgileri çıkarılamadı. Lütfen bağlantınızı kontrol edin.",
"analyzing_preparing": "Analiz ediliyor (0%)... İstek hazırlanıyor", "analyzing_preparing": "İstek hazırlanıyor...",
"analyzing_extracting_basic": "Analiz ediliyor (15%)... Temel bilgiler çıkarılıyor", "analyzing_extracting_basic": "Temel bilgiler çıkarılıyor...",
"analyzing_extracting_detailed": "Analiz ediliyor (30%)... Ayrıntılı bilgiler çıkarılıyor", "analyzing_extracting_detailed": "Ayrıntılı bilgiler çıkarılıyor...",
"analyzing_processing_video": "Analiz ediliyor (45%)... Video verisi işleniyor", "analyzing_processing_video": "Video verisi işleniyor...",
"analyzing_processing_formats": "Analiz ediliyor (60%)... Formatlar işleniyor", "analyzing_processing_formats": "Formatlar işleniyor...",
"analyzing_loading_thumbnail": "Analiz ediliyor (75%)... Küçük resim yükleniyor", "analyzing_loading_thumbnail": "Küçük resim yükleniyor...",
"analyzing_processing_subtitles": "Analiz ediliyor (85%)... Altyazılar işleniyor", "analyzing_processing_subtitles": "Altyazılar işleniyor...",
"analyzing_updating_table": "Analiz ediliyor (95%)... Format tablosu güncelleniyor", "analyzing_updating_table": "Format tablosu güncelleniyor...",
"analysis_complete": "Analiz tamamlandı!", "analysis_complete": "Analiz tamamlandı!",
"analyzing_extracting_ytdlp": "Analiz ediliyor (30%)... Bilgiler çıkarılıyor", "analyzing_fetching_first_video": "İlk video için formatlar alınıyor...",
"analyzing_processing_data": "Analiz ediliyor (60%)... Veriler işleniyor", "analyzing_extracting_ytdlp": "Bilgiler çıkarılıyor...",
"analyzing_processing_formats_ytdlp": "Analiz ediliyor (75%)... Formatlar işleniyor", "analyzing_processing_data": "Veriler işleniyor...",
"analyzing_loading_thumbnail_ytdlp": "Analiz ediliyor (85%)... Küçük resim yükleniyor", "analyzing_processing_formats_ytdlp": "Formatlar işleniyor...",
"analyzing_processing_subtitles_ytdlp": "Analiz ediliyor (90%)... Altyazılar işleniyor", "analyzing_loading_thumbnail_ytdlp": "Küçük resim yükleniyor...",
"analyzing_processing_subtitles_ytdlp": "Altyazılar işleniyor...",
"select_subtitles": "Altyazı seç...", "select_subtitles": "Altyazı seç...",
"sponsorblock_categories": "SponsorBlock kategorileri...", "sponsorblock_categories": "SponsorBlock kategorileri...",
"invalid_url_or_enter": "Geçersiz URL veya lütfen bir URL girin.", "invalid_url_or_enter": "Geçersiz URL veya lütfen bir URL girin.",
"zero_selected": "0 seçildi", "zero_selected": "0 seçildi",
"analyze_first_tooltip": "Lütfen önce videoyu analiz edin", "analyze_first_tooltip": "Lütfen önce videoyu analiz edin",
"audio_mode_disabled": "Yalnızca ses modunda kullanılamaz", "audio_mode_disabled": "Yalnızca ses modunda kullanılamaz",
"select_subtitles_first": "Lütfen önce altyazıları seçin" "select_subtitles_first": "Lütfen önce altyazıları seçin",
"settings_tooltip": "Mevcut Yol: {path}\nHız Sınırı: {speed_limit}",
"speed_limit_none": "Yok",
"open_folder_error": "Klasör açılamadı: {error}",
"time_range_set": "Bölüm ayarlandı: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "Sponsor", "sponsor": "Sponsor",
@@ -408,7 +437,10 @@
"ytdlp_failed": "Hata: yt-dlp başarısız oldu: {error}", "ytdlp_failed": "Hata: yt-dlp başarısız oldu: {error}",
"parse_failed": "Hata: yt-dlp çıktısı ayrıştırılamadı: {error}", "parse_failed": "Hata: yt-dlp çıktısı ayrıştırılamadı: {error}",
"analysis_failed": "Hata: Analiz başarısız oldu: {error}", "analysis_failed": "Hata: Analiz başarısız oldu: {error}",
"generic_error": "Hata: {error}" "generic_error": "Hata: {error}",
"download_failed_return_code_conflict": "İndirme {return_code} dönüş koduyla başarısız oldu. Bunun nedeni birden fazla yt-dlp kurulumunun çakışması olabilir. Sistem kurulumlu yt-dlp'yi (örn. snap veya apt) kaldırıp uygulamayı yeniden başlatmayı deneyin.",
"download_failed_return_code": "İndirme {return_code} dönüş koduyla başarısız oldu",
"direct_command_error": "Doğrudan komutta hata: {error}"
}, },
"update_dialog": { "update_dialog": {
"title": "Güncelleme Mevcut", "title": "Güncelleme Mevcut",
@@ -442,7 +474,8 @@
"next_check": "Sonraki kontrol: {time}", "next_check": "Sonraki kontrol: {time}",
"next_check_error": "Sonraki kontrol: Hesaplama hatası", "next_check_error": "Sonraki kontrol: Hesaplama hatası",
"checking": "🔄 Kontrol ediliyor...", "checking": "🔄 Kontrol ediliyor...",
"check_now": "🔍 Şimdi güncellemeleri kontrol et" "check_now": "🔍 Şimdi güncellemeleri kontrol et",
"current_version": "Mevcut yt-dlp sürümü: {version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL boş olamaz", "empty_url": "URL boş olamaz",
@@ -491,7 +524,41 @@
"one_entry": "1 indirme", "one_entry": "1 indirme",
"redownload_confirm_title": "Tekrar İndir?", "redownload_confirm_title": "Tekrar İndir?",
"redownload_confirm_message": "Tekrar indir?\n\n{title}", "redownload_confirm_message": "Tekrar indir?\n\n{title}",
"redownload_started": "İndirme başladı" "redownload_started": "İndirme başladı",
"no_url_error": "Geçmiş kaydında URL bulunamadı",
"redownload_failed": "Yeniden indirme başlatılamadı: {error}"
},
"ffmpeg": {
"installation_title": "FFmpeg Kurulumu",
"installation_message": "YTSage, videoları işlemek için FFmpeg'e ihtiyaç duyar.\n\nAşağıdan bir kurulum seçeneği seçin:",
"install_button": "FFmpeg Kur",
"manual_guide": "Manuel Kılavuz",
"installation_failed": "FFmpeg kurulumu bir sorunla karşılaştı."
},
"ytdlp_setup": {
"required_title": "yt-dlp Kurulumu Gerekli",
"description": "YTSage, video indirmek için yt-dlp'ye ihtiyaç duyar.<br><br>yt-dlp uygulamanın yerel dizininde bulunamadı. YTSage'in {os_name} sisteminiz için yt-dlp'yi kurması gerekiyor.<br><br>Lütfen aşağıdan bir seçenek seçin:",
"option_auto": "Otomatik indir (Önerilen)",
"option_manual": "Yolu manuel seç",
"setup_button": "yt-dlp Kur",
"downloading": "yt-dlp indiriliyor...",
"success": "yt-dlp başarıyla yüklendi!",
"error": "Hata: {error}",
"download_failed_title": "İndirme Başarısız",
"download_failed_message": "yt-dlp indirilemedi: {error}",
"select_executable_title": "yt-dlp çalıştırılabilir dosyasını seç",
"copied_to": "yt-dlp başarıyla {path} konumuna kopyalandı",
"setup_error_title": "Kurulum Hatası",
"copy_error": "yt-dlp uygulama dizinine kopyalanırken hata: {error}",
"invalid_executable_title": "Geçersiz Çalıştırılabilir",
"invalid_executable_message": "Seçilen dosya geçerli bir yt-dlp çalıştırılabilir dosyası değil.",
"verify_error": "yt-dlp çalıştırılabilir dosyası doğrulanırken hata: {error}",
"setup_failed_title": "Kurulum Başarısız",
"setup_failed_message": "yt-dlp kurulumu başarısız oldu. Bazı özellikler düzgün çalışmayabilir.",
"success_dialog_title": "yt-dlp Kurulumu",
"success_dialog_message": "yt-dlp başarıyla şu konuma yapılandırıldı:\n{path}",
"file_filter_windows": "Çalıştırılabilir Dosyalar (*.exe)",
"file_filter_all": "Tüm Dosyalar (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "FFmpeg Sürüm Denetçisi", "title": "FFmpeg Sürüm Denetçisi",
+77 -20
View File
@@ -57,6 +57,7 @@
"audio_only_resolution": "仅音频" "audio_only_resolution": "仅音频"
}, },
"buttons": { "buttons": {
"reset": "重置",
"download": "下载", "download": "下载",
"pause": "暂停", "pause": "暂停",
"resume": "恢复", "resume": "恢复",
@@ -93,7 +94,8 @@
"select_subtitles": "选择字幕", "select_subtitles": "选择字幕",
"filter_languages_placeholder": "过滤语言(例如:en, zh...", "filter_languages_placeholder": "过滤语言(例如:en, zh...",
"no_subtitles_available": "无可用字幕", "no_subtitles_available": "无可用字幕",
"matching": "匹配" "matching": "匹配",
"ytdlp_log_title": "yt-dlp 日志"
}, },
"tabs": { "tabs": {
"cookies": "使用 Cookie 登录", "cookies": "使用 Cookie 登录",
@@ -159,7 +161,13 @@
"invalid_main_url": "主代理网址格式无效", "invalid_main_url": "主代理网址格式无效",
"invalid_geo_url": "地理代理网址格式无效", "invalid_geo_url": "地理代理网址格式无效",
"main_configured": "主代理已配置", "main_configured": "主代理已配置",
"geo_configured": "地理代理已配置" "geo_configured": "地理代理已配置",
"set_title": "已设置代理",
"set_message": "主代理已设置并保存: {proxy}",
"geo_set_title": "已设置地理代理",
"geo_set_message": "地理验证代理已设置并保存: {proxy}",
"cleared_title": "代理设置已清除",
"cleared_message": "所有代理设置已清除并保存。"
}, },
"download": { "download": {
"preparing": "正在准备下载...", "preparing": "正在准备下载...",
@@ -225,6 +233,8 @@
"system_info": "系统信息", "system_info": "系统信息",
"loading": "🔄 正在加载系统信息...", "loading": "🔄 正在加载系统信息...",
"refresh": "🔄", "refresh": "🔄",
"open_logs": "📂 日志",
"logs_tooltip": "打开应用程序日志文件夹",
"refreshing": "🔄 正在刷新...", "refreshing": "🔄 正在刷新...",
"refresh_failed": "刷新失败", "refresh_failed": "刷新失败",
"refresh_failed_message": "无法刷新版本信息。", "refresh_failed_message": "无法刷新版本信息。",
@@ -277,6 +287,8 @@
"ytdlp_channel_switched": "✅ 成功切换到 {channel} 渠道!", "ytdlp_channel_switched": "✅ 成功切换到 {channel} 渠道!",
"ytdlp_channel_switch_failed": "❌ 切换渠道失败:{error}", "ytdlp_channel_switch_failed": "❌ 切换渠道失败:{error}",
"ytdlp_current_channel": "当前渠道:{channel}", "ytdlp_current_channel": "当前渠道:{channel}",
"app_updates_title": "YTSage 更新",
"check_beta_updates": "接收测试版更新",
"auto_update_title": "自动更新设置", "auto_update_title": "自动更新设置",
"auto_update_header": "🔄 自动更新设置", "auto_update_header": "🔄 自动更新设置",
"auto_update_description": "为 yt-dlp 配置自动更新,以确保您始终拥有最新功能和错误修复。", "auto_update_description": "为 yt-dlp 配置自动更新,以确保您始终拥有最新功能和错误修复。",
@@ -307,7 +319,9 @@
"audio_format_wav": "WAV(未压缩)", "audio_format_wav": "WAV(未压缩)",
"audio_format_opus": "Opus(高效)", "audio_format_opus": "Opus(高效)",
"audio_format_m4a": "M4AApple", "audio_format_m4a": "M4AApple",
"audio_format_vorbis": "Vorbis(开放)" "audio_format_vorbis": "Vorbis(开放)",
"filename_format": "输出文件名格式",
"filename_format_help": "可用变量: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s。支持标准的 yt-dlp 输出模板语法。"
}, },
"main_ui": { "main_ui": {
"url_placeholder": "输入 YouTube 视频或播放列表网址", "url_placeholder": "输入 YouTube 视频或播放列表网址",
@@ -326,27 +340,32 @@
"browser_cookies_selected_message": "将从以下浏览器提取 Cookie{browser}", "browser_cookies_selected_message": "将从以下浏览器提取 Cookie{browser}",
"error_no_format_info": "错误:无可用格式信息。", "error_no_format_info": "错误:无可用格式信息。",
"error_extract_info": "错误:无法提取基本视频信息。请检查您的链接。", "error_extract_info": "错误:无法提取基本视频信息。请检查您的链接。",
"analyzing_preparing": "分析中 (0%)... 正在准备请求", "analyzing_preparing": "正在准备请求...",
"analyzing_extracting_basic": "分析中 (15%)... 正在提取基本信息", "analyzing_extracting_basic": "正在提取基本信息...",
"analyzing_extracting_detailed": "分析中 (30%)... 正在提取详细信息", "analyzing_extracting_detailed": "正在提取详细信息...",
"analyzing_processing_video": "分析中 (45%)... 正在处理视频数据", "analyzing_processing_video": "正在处理视频数据...",
"analyzing_processing_formats": "分析中 (60%)... 正在处理格式", "analyzing_processing_formats": "正在处理格式...",
"analyzing_loading_thumbnail": "分析中 (75%)... 正在加载缩略图", "analyzing_loading_thumbnail": "正在加载缩略图...",
"analyzing_processing_subtitles": "分析中 (85%)... 正在处理字幕", "analyzing_processing_subtitles": "正在处理字幕...",
"analyzing_updating_table": "分析中 (95%)... 正在更新格式表", "analyzing_updating_table": "正在更新格式表...",
"analysis_complete": "分析完成!", "analysis_complete": "分析完成!",
"analyzing_extracting_ytdlp": "分析中 (30%)... 提取信息", "analyzing_fetching_first_video": "正在获取第一个视频的格式...",
"analyzing_processing_data": "分析中 (60%)... 正在处理数据", "analyzing_extracting_ytdlp": "提取信息...",
"analyzing_processing_formats_ytdlp": "分析中 (75%)... 正在处理格式", "analyzing_processing_data": "正在处理数据...",
"analyzing_loading_thumbnail_ytdlp": "分析中 (85%)... 正在加载缩略图", "analyzing_processing_formats_ytdlp": "正在处理格式...",
"analyzing_processing_subtitles_ytdlp": "分析中 (90%)... 正在处理字幕", "analyzing_loading_thumbnail_ytdlp": "正在加载缩略图...",
"analyzing_processing_subtitles_ytdlp": "正在处理字幕...",
"select_subtitles": "选择字幕...", "select_subtitles": "选择字幕...",
"sponsorblock_categories": "SponsorBlock 分类...", "sponsorblock_categories": "SponsorBlock 分类...",
"invalid_url_or_enter": "无效的URL或请输入URL。", "invalid_url_or_enter": "无效的URL或请输入URL。",
"zero_selected": "已选择 0 个", "zero_selected": "已选择 0 个",
"analyze_first_tooltip": "请先分析视频", "analyze_first_tooltip": "请先分析视频",
"audio_mode_disabled": "在纯音频模式下不可用", "audio_mode_disabled": "在纯音频模式下不可用",
"select_subtitles_first": "请先选择字幕" "select_subtitles_first": "请先选择字幕",
"settings_tooltip": "当前路径: {path}\n速度限制: {speed_limit}",
"speed_limit_none": "无",
"open_folder_error": "无法打开文件夹: {error}",
"time_range_set": "已设置区间: {section}"
}, },
"sponsorblock": { "sponsorblock": {
"sponsor": "赞助商", "sponsor": "赞助商",
@@ -408,7 +427,10 @@
"ytdlp_failed": "错误:yt-dlp失败:{error}", "ytdlp_failed": "错误:yt-dlp失败:{error}",
"parse_failed": "错误:解析yt-dlp输出失败:{error}", "parse_failed": "错误:解析yt-dlp输出失败:{error}",
"analysis_failed": "错误:分析失败:{error}", "analysis_failed": "错误:分析失败:{error}",
"generic_error": "错误:{error}" "generic_error": "错误:{error}",
"download_failed_return_code_conflict": "下载失败,返回码 {return_code}。这可能是由于存在多个 yt-dlp 安装导致冲突。请卸载系统中安装的 yt-dlp(例如通过 snap 或 apt),然后重启应用。",
"download_failed_return_code": "下载失败,返回码 {return_code}",
"direct_command_error": "直接命令出错:{error}"
}, },
"update_dialog": { "update_dialog": {
"title": "有可用更新", "title": "有可用更新",
@@ -442,7 +464,8 @@
"next_check": "下次检查: {time}", "next_check": "下次检查: {time}",
"next_check_error": "下次检查: 计算错误", "next_check_error": "下次检查: 计算错误",
"checking": "🔄 检查中...", "checking": "🔄 检查中...",
"check_now": "🔍 立即检查更新" "check_now": "🔍 立即检查更新",
"current_version": "当前 yt-dlp 版本:{version}"
}, },
"url_validation": { "url_validation": {
"empty_url": "URL不能为空", "empty_url": "URL不能为空",
@@ -491,7 +514,41 @@
"one_entry": "1 个下载", "one_entry": "1 个下载",
"redownload_confirm_title": "重新下载?", "redownload_confirm_title": "重新下载?",
"redownload_confirm_message": "重新下载?\n\n{title}", "redownload_confirm_message": "重新下载?\n\n{title}",
"redownload_started": "已开始下载" "redownload_started": "已开始下载",
"no_url_error": "历史记录中未找到 URL",
"redownload_failed": "无法开始重新下载: {error}"
},
"ffmpeg": {
"installation_title": "FFmpeg 安装",
"installation_message": "YTSage 需要 FFmpeg 来处理视频。\n\n请选择下面的安装选项:",
"install_button": "安装 FFmpeg",
"manual_guide": "手动指南",
"installation_failed": "FFmpeg 安装遇到问题。"
},
"ytdlp_setup": {
"required_title": "需要设置 yt-dlp",
"description": "YTSage 需要 yt-dlp 来下载视频。<br><br>在应用本地目录中未找到 yt-dlp。YTSage 需要为你的 {os_name} 系统设置 yt-dlp。<br><br>请选择下面的选项:",
"option_auto": "自动下载(推荐)",
"option_manual": "手动选择路径",
"setup_button": "设置 yt-dlp",
"downloading": "正在下载 yt-dlp...",
"success": "yt-dlp 已成功安装!",
"error": "错误:{error}",
"download_failed_title": "下载失败",
"download_failed_message": "无法下载 yt-dlp{error}",
"select_executable_title": "选择 yt-dlp 可执行文件",
"copied_to": "yt-dlp 已成功复制到 {path}",
"setup_error_title": "设置错误",
"copy_error": "将 yt-dlp 复制到应用目录时出错:{error}",
"invalid_executable_title": "无效的可执行文件",
"invalid_executable_message": "所选文件似乎不是有效的 yt-dlp 可执行文件。",
"verify_error": "验证 yt-dlp 可执行文件时出错:{error}",
"setup_failed_title": "设置失败",
"setup_failed_message": "无法设置 yt-dlp。某些功能可能无法正常工作。",
"success_dialog_title": "yt-dlp 设置",
"success_dialog_message": "yt-dlp 已成功配置到:\n{path}",
"file_filter_windows": "可执行文件 (*.exe)",
"file_filter_all": "所有文件 (*)"
}, },
"ffmpeg_updater": { "ffmpeg_updater": {
"title": "FFmpeg 版本检查器", "title": "FFmpeg 版本检查器",
+34
View File
@@ -0,0 +1,34 @@
import sys
from PySide6.QtWidgets import QApplication, QMessageBox
from .utils.ytsage_logger import logger
from .gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main
def show_error_dialog(message):
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setText("Application Error")
error_dialog.setInformativeText(message)
error_dialog.setWindowTitle("Error")
error_dialog.exec()
def main():
try:
logger.info("Starting YTSage application")
app = QApplication(sys.argv)
window = YTSageApp() # Instantiate the main application class
window.show()
logger.info("Application window shown, entering main loop")
sys.exit(app.exec())
except Exception as e:
logger.critical(f"Critical application error: {e}", exc_info=True)
show_error_dialog(f"Critical error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
"""
Utility modules for YTSage.
This package contains shared utilities, constants, logging, and configuration management.
"""
@@ -20,7 +20,7 @@ Features
Usage Usage
----- -----
from src.utils.ytsage_config_manager import ConfigManager from .ytsage_config_manager import ConfigManager
# Load settings (auto-loads if not already loaded) # Load settings (auto-loads if not already loaded)
download_path = ConfigManager.get("download_path") download_path = ConfigManager.get("download_path")
@@ -54,8 +54,8 @@ import threading
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from src.utils.ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR from .ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR
from src.utils.ytsage_logger import logger from .ytsage_logger import logger
class ConfigManager: class ConfigManager:
@@ -83,6 +83,7 @@ class ConfigManager:
"geo_proxy_url": None, "geo_proxy_url": None,
"auto_update_ytdlp": True, "auto_update_ytdlp": True,
"auto_update_frequency": "daily", "auto_update_frequency": "daily",
"check_beta_updates": False,
"last_update_check": 0, "last_update_check": 0,
"language": "en", "language": "en",
"ytdlp_channel": "stable", "ytdlp_channel": "stable",
@@ -90,6 +91,7 @@ class ConfigManager:
"preferred_output_format": "mp4", "preferred_output_format": "mp4",
"force_audio_format": False, "force_audio_format": False,
"preferred_audio_format": "best", "preferred_audio_format": "best",
"filename_format": "%(title)s_%(resolution)s.%(ext)s",
"cached_versions": { "cached_versions": {
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, "ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, "ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
@@ -70,7 +70,7 @@ def get_asset_path(asset_relative_path: str) -> Path:
# Fallback to relative path (for development environment) # Fallback to relative path (for development environment)
current_file = Path(__file__) current_file = Path(__file__)
# Go up from src/utils to ytsage root, then to asset # Go up from utils to ytsage root, then to asset
ytsage_root = current_file.parent.parent.parent ytsage_root = current_file.parent.parent.parent
asset_path = ytsage_root / asset_relative_path asset_path = ytsage_root / asset_relative_path
@@ -182,6 +182,30 @@ FFMPEG_ZIP_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essen
FFMPEG_7Z_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.ver" 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" 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 __name__ == "__main__":
# If this file is run directly, print directory information; if imported, create the necessary directories for the application. # 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. # for debug, to check os specific variable which can be different based on os.
+428
View File
@@ -0,0 +1,428 @@
"""
History Manager Module
======================
This module provides **thread-safe** centralized management for download
history in YTSage using SQLite for high performance and scalability.
Features
--------
- Scalable: Uses SQLite instead of parsing potentially large JSON files.
- Thread-safe: Handles database connections safely.
- Migration: Automatically migrates legacy JSON history to SQLite.
- CRUD: Create, Read, Delete, Clear operations for history entries.
Usage
-----
from .ytsage_history_manager import HistoryManager
# Add a download to history
HistoryManager.add_entry(...)
# Get all history entries
history = HistoryManager.get_all_entries()
# Get recent entries (limit + offset support planned)
# Remove an entry
HistoryManager.remove_entry(entry_id)
# Clear all history
HistoryManager.clear_history()
"""
import json
import sqlite3
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from .ytsage_constants import APP_HISTORY_FILE, APP_DATA_DIR
from .ytsage_logger import logger
class HistoryManager:
"""
Thread-safe history manager for YTSage using SQLite.
"""
_lock = threading.RLock()
# Define DB file next to the old JSON file
_db_file = APP_DATA_DIR / "ytsage_history.db"
_connection = None
_initialized = False
@classmethod
def _init_db(cls):
"""Initialize the database: create table and migrate if needed."""
if cls._initialized:
return
with cls._lock:
# Check if file exists to know if we need to migrate or just create schema
db_exists = cls._db_file.exists()
legacy_json_exists = APP_HISTORY_FILE.exists()
try:
# Ensure directory exists
cls._db_file.parent.mkdir(parents=True, exist_ok=True)
# We use a persistent connection to avoid churn
if cls._connection is None:
cls._connection = sqlite3.connect(cls._db_file, check_same_thread=False)
cls._connection.row_factory = sqlite3.Row
cursor = cls._connection.cursor()
# Create table
cursor.execute("""
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
title TEXT,
url TEXT,
channel TEXT,
file_path TEXT,
download_date TEXT,
file_size INTEGER,
thumbnail_url TEXT,
format_id TEXT,
resolution TEXT,
is_audio_only INTEGER,
duration TEXT,
options TEXT,
timestamp REAL
)
""")
# Index for faster sorting by date
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_timestamp
ON history (timestamp DESC)
""")
# Indexes for faster search (title, channel, url)
# This prevents full table scans during search
cursor.execute("CREATE INDEX IF NOT EXISTS idx_title ON history (title)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_channel ON history (channel)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_url ON history (url)")
cls._connection.commit()
# If we just created the DB and have a JSON file, migrate
if not db_exists and legacy_json_exists:
cls._migrate_legacy_json()
cls._initialized = True
except sqlite3.Error as e:
logger.error(f"Failed to initialize history database: {e}")
@classmethod
def _migrate_legacy_json(cls):
"""Migrate legacy JSON history to SQLite."""
logger.info("Migrating legacy history JSON to SQLite...")
try:
with open(APP_HISTORY_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
count = 0
# Use the persistent connection
conn = cls._get_connection()
try:
with conn: # Transaction
cursor = conn.cursor()
for entry in data:
try:
# Safely extract download_options logic if complex
options_json = json.dumps(entry.get("download_options", {}))
# Construct timestamp from isoformat if missing
ts = entry.get("timestamp")
if not ts and "download_date" in entry:
try:
dt = datetime.fromisoformat(entry["download_date"])
ts = dt.timestamp()
except Exception:
ts = time.time()
cursor.execute("""
INSERT OR IGNORE INTO history (
id, title, url, channel, file_path, download_date,
file_size, thumbnail_url, format_id, resolution,
is_audio_only, duration, options, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
entry.get("id", str(int(time.time()*1000))),
entry.get("title", ""),
entry.get("url", ""),
entry.get("channel", "Unknown"),
entry.get("file_path", ""),
entry.get("download_date", ""),
entry.get("file_size", 0),
entry.get("thumbnail_url", ""),
entry.get("format_id", ""),
entry.get("resolution", ""),
1 if entry.get("is_audio_only") else 0,
entry.get("duration", ""),
options_json,
ts or time.time()
))
count += 1
except Exception as e:
logger.error(f"Skipped invalid entry during migration: {e}")
logger.info(f"Successfully migrated {count} history entries.")
# Rename old JSON to .bak to avoid re-migration, or keep as backup
try:
APP_HISTORY_FILE.rename(APP_HISTORY_FILE.with_suffix(".json.bak"))
except Exception as e:
logger.warning(f"Could not rename legacy history file: {e}")
except Exception as e:
logger.error(f"Migration transaction failed: {e}")
except Exception as e:
logger.error(f"Migration failed: {e}")
@classmethod
def _get_connection(cls):
"""Get the persistent database connection."""
cls._init_db()
if cls._connection is None:
# Should be created in _init_db, but just in case
cls._connection = sqlite3.connect(cls._db_file, check_same_thread=False)
cls._connection.row_factory = sqlite3.Row
return cls._connection
@classmethod
def get_all_entries(cls, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Get all history entries, sorted by most recent first.
Args:
limit: Optional limit on number of entries to return (most recent first)
Returns:
List of dictionary entries.
"""
entries = []
try:
with cls._lock: # Lock for simple concurrency safety
# Use persistent connection
conn = cls._get_connection()
# conn.row_factory is already set in _init_db/_get_connection
cursor = conn.cursor()
query = "SELECT * FROM history ORDER BY timestamp DESC"
params = ()
if limit is not None:
query += " LIMIT ?"
params = (limit,)
cursor.execute(query, params)
rows = cursor.fetchall()
for row in rows:
entry = dict(row)
# Convert boolean back
entry["is_audio_only"] = bool(entry["is_audio_only"])
# Parse options JSON
try:
entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {}
except json.JSONDecodeError:
entry["download_options"] = {}
del entry["options"] # Remove internal column
entries.append(entry)
except Exception as e:
logger.error(f"Error fetching history: {e}")
return entries
@classmethod
def get_entry(cls, entry_id: str) -> Optional[Dict[str, Any]]:
"""
Get a specific history entry by ID.
Args:
entry_id: The unique entry ID
Returns:
Entry dictionary or None if not found
"""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM history WHERE id = ?", (entry_id,))
row = cursor.fetchone()
if row:
entry = dict(row)
entry["is_audio_only"] = bool(entry["is_audio_only"])
try:
entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {}
except json.JSONDecodeError:
entry["download_options"] = {}
del entry["options"]
return entry
return None
except Exception as e:
logger.error(f"Error fetching entry {entry_id}: {e}")
return None
@classmethod
def add_entry(
cls,
title: str,
url: str,
thumbnail_url: Optional[str],
file_path: str,
format_id: str,
is_audio_only: bool,
resolution: str,
file_size: Optional[int] = None,
channel: Optional[str] = None,
duration: Optional[str] = None,
download_options: Optional[Dict[str, Any]] = None,
) -> str:
"""
Add a new entry to the download history.
"""
if download_options is None:
download_options = {}
timestamp = time.time()
# Ensure unique ID
unique_id = f"{int(timestamp * 1000)}"
download_date = datetime.fromtimestamp(timestamp).isoformat()
# Determine file size if not provided
if file_size is None:
try:
p = Path(file_path)
if p.exists():
file_size = p.stat().st_size
except Exception:
file_size = 0
# Allow None for optional strings
channel = channel or "Unknown"
duration = duration or ""
thumbnail_url = thumbnail_url or ""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO history (
id, title, url, channel, file_path, download_date,
file_size, thumbnail_url, format_id, resolution,
is_audio_only, duration, options, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
unique_id,
title,
url,
channel,
str(file_path),
download_date,
file_size,
thumbnail_url,
format_id,
resolution,
1 if is_audio_only else 0,
duration,
json.dumps(download_options),
timestamp
))
conn.commit()
logger.info(f"Added history entry: {title}")
return unique_id
except Exception as e:
logger.error(f"Error adding history entry: {e}")
return ""
@classmethod
def remove_entry(cls, entry_id: str) -> bool:
"""Remove an entry from history by ID."""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM history WHERE id = ?", (entry_id,))
if cursor.rowcount > 0:
conn.commit()
logger.info(f"Removed history entry: {entry_id}")
return True
return False
except Exception as e:
logger.error(f"Error removing history entry: {e}")
return False
@classmethod
def clear_history(cls) -> int:
"""Clear all history entries."""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM history")
count = cursor.rowcount
conn.commit()
logger.info("History cleared")
return count
except Exception as e:
logger.error(f"Error clearing history: {e}")
return 0
@classmethod
def search_entries(cls, query: str) -> List[Dict[str, Any]]:
"""
Search history entries by title, channel, or URL.
Args:
query: Search query string
Returns:
List of matching history entries
"""
if not query:
return cls.get_all_entries()
entries = []
try:
search_pattern = f"%{query}%"
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM history
WHERE title LIKE ? OR channel LIKE ? OR url LIKE ?
ORDER BY timestamp DESC
""", (search_pattern, search_pattern, search_pattern))
rows = cursor.fetchall()
for row in rows:
entry = dict(row)
entry["is_audio_only"] = bool(entry["is_audio_only"])
try:
entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {}
except json.JSONDecodeError:
entry["download_options"] = {}
del entry["options"]
entries.append(entry)
except Exception as e:
logger.error(f"Error searching history: {e}")
return entries
@@ -15,7 +15,7 @@ Features
Usage Usage
----- -----
from src.utils.ytsage_localization import LocalizationManager from .ytsage_localization import LocalizationManager
# Get localized text # Get localized text
text = LocalizationManager.get_text("download.ready") text = LocalizationManager.get_text("download.ready")
@@ -33,7 +33,7 @@ import threading
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from src.utils.ytsage_logger import logger from .ytsage_logger import logger
class LocalizationManager: class LocalizationManager:
@@ -46,7 +46,7 @@ class LocalizationManager:
_lock = threading.RLock() _lock = threading.RLock()
_current_language = "en" _current_language = "en"
_languages: Dict[str, Dict[str, Any]] = {} _languages: Dict[str, Dict[str, Any]] = {}
_languages_dir = Path(__file__).parent.parent.parent / "languages" _languages_dir = Path(__file__).parent.parent / "languages"
# Fallback English strings embedded in code # Fallback English strings embedded in code
_fallback_strings = { _fallback_strings = {
@@ -108,6 +108,16 @@ class LocalizationManager:
}, },
"formats": { "formats": {
"show_formats": "Show formats:" "show_formats": "Show formats:"
},
"errors": {
"download_failed_return_code_conflict": "Download failed with return code {return_code}. This may be due to a conflict with multiple yt-dlp installations. Try uninstalling any system-installed yt-dlp (e.g. through snap or apt) and restart the application.",
"download_failed_return_code": "Download failed with return code {return_code}",
"direct_command_error": "Error in direct command: {error}"
},
"about": {
"open_logs": "📂 Logs",
"logs_tooltip": "Open application logs folder",
"refresh": "🔄"
} }
} }
@@ -9,7 +9,7 @@ import sys
from loguru import logger from loguru import logger
from src.utils.ytsage_constants import APP_LOG_DIR, IS_FROZEN from .ytsage_constants import APP_LOG_DIR, IS_FROZEN
# Separate configs for each handler # Separate configs for each handler
CONSOLE_CONFIG = { CONSOLE_CONFIG = {