Merge branch 'beta'
@@ -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.
|
||||
|
||||
### 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.
|
||||
- **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.
|
||||
- **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`, `Build macOS Release`, or `Build PyPI Package` individually.
|
||||
|
||||
### Build Process
|
||||
1. **Setup**: Uses Python 3.13 on all platforms
|
||||
@@ -43,18 +43,26 @@ The workflow creates the following files based on the platform:
|
||||
#### Windows
|
||||
- `YTSage-v{version}-portable.zip` - Standard portable version
|
||||
- `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
|
||||
- `YTSage-v{version}-{arch}.AppImage` - AppImage portable (x86_64, aarch64)
|
||||
- `YTSage-v{version}-{arch}.rpm` - RPM package
|
||||
- `YTSage-v{version}-{arch}.deb` - Debian package
|
||||
- `YTSage-v{version}-{arch}.flatpak` - Flatpak bundle
|
||||
|
||||
#### macOS
|
||||
- `YTSage-v{version}-{arch}.app.zip` - Zipped application bundle (x64, arm64)
|
||||
- `YTSage-v{version}-{arch}.dmg` - Disk image installer
|
||||
- `YTSage-v{version}-arm64.app.zip` - Zipped application bundle
|
||||
- `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
|
||||
|
||||
- **PyPI**: Standard Python build system (Wheel & Source)
|
||||
### Multi-Platform Support
|
||||
- **Windows**: Uses PowerShell scripts with cx_Freeze
|
||||
- **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
|
||||
- `build-windows.yml` - Windows builds logic
|
||||
- `build-linux.yml` - Linux builds logic
|
||||
- `build-pypi.yml` - PyPI build logic
|
||||
- `build-macos.yml` - macOS builds logic
|
||||
|
||||
### Key Configuration Options
|
||||
|
||||
@@ -45,14 +45,10 @@ assignees: ''
|
||||
Attach screenshots (for GUI issues) or terminal logs (for CLI errors).
|
||||
|
||||
To collect log files:
|
||||
1. Go to the logs folder:
|
||||
- Windows: %LOCALAPPDATA%\YTSage\logs
|
||||
- macOS: ~/Library/Application Support/YTSage/logs
|
||||
- Linux: ~/.local/share/YTSage/logs
|
||||
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
|
||||
1. Open YTSage and reproduce the issue.
|
||||
2. Click the **About** button.
|
||||
3. Click **Logs** (📂) to open the logs folder.
|
||||
4. Attach `ytsage.log` and `ytsage_error.log`.
|
||||
|
||||
Use ``` to format logs:
|
||||
-->
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
path: |
|
||||
venv
|
||||
~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
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
|
||||
|
||||
- name: Prepare build variables
|
||||
@@ -82,6 +82,13 @@ jobs:
|
||||
- name: Create cx_Freeze setup script (Linux)
|
||||
shell: bash
|
||||
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'
|
||||
import os
|
||||
import sys
|
||||
@@ -92,40 +99,48 @@ jobs:
|
||||
|
||||
# Prepare include_files list
|
||||
include_files_list = [
|
||||
("src", "src"),
|
||||
("assets/branding/icons", "lib/assets/branding/icons"),
|
||||
("assets/Icon", "lib/assets/Icon"),
|
||||
("assets/sound", "lib/assets/sound"),
|
||||
("languages", "lib/languages"),
|
||||
("ytsage/assets/Icon", "lib/assets/Icon"),
|
||||
("ytsage/assets/sound", "lib/assets/sound"),
|
||||
("ytsage/languages", "lib/languages"),
|
||||
("branding/icons", "lib/assets/branding/icons"),
|
||||
("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(
|
||||
optimize=2,
|
||||
silent=True,
|
||||
packages=[
|
||||
"ytsage",
|
||||
"PySide6.QtCore",
|
||||
"PySide6.QtGui",
|
||||
"PySide6.QtWidgets",
|
||||
"PySide6.QtMultimedia",
|
||||
"PySide6.QtNetwork",
|
||||
"PySide6.QtDBus",
|
||||
"PySide6.QtSvg",
|
||||
"requests",
|
||||
"PIL",
|
||||
"packaging",
|
||||
"markdown",
|
||||
"pyglet",
|
||||
"loguru",
|
||||
"setuptools",
|
||||
],
|
||||
zip_include_packages=[
|
||||
"PySide6",
|
||||
"shiboken6",
|
||||
"requests",
|
||||
"PIL",
|
||||
"packaging",
|
||||
],
|
||||
excludes=[
|
||||
"PySide6.QtBluetooth",
|
||||
"PySide6.QtNetwork",
|
||||
"PySide6.QtOpenGL",
|
||||
"PySide6.QtPrintSupport",
|
||||
"PySide6.QtSvg",
|
||||
"PySide6.QtTest",
|
||||
"PySide6.QtXml",
|
||||
"PySide6.QtSql",
|
||||
"PySide6.QtHelp",
|
||||
"PySide6.QtMultimedia",
|
||||
"PySide6.QtQml",
|
||||
"PySide6.QtQuick",
|
||||
"PySide6.QtWebEngineCore",
|
||||
@@ -140,6 +155,9 @@ jobs:
|
||||
"unittest",
|
||||
"test",
|
||||
"tests",
|
||||
"pydoc",
|
||||
"doctest",
|
||||
"email",
|
||||
],
|
||||
include_files=include_files_list,
|
||||
# Bundle all dependencies - avoid system library references
|
||||
@@ -151,9 +169,9 @@ jobs:
|
||||
|
||||
executables = [
|
||||
Executable(
|
||||
script="main.py",
|
||||
script="ytsage_entry.py",
|
||||
target_name="ytsage",
|
||||
icon="assets/branding/icons/icon.png",
|
||||
icon="branding/icons/icon.png",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -194,18 +212,101 @@ jobs:
|
||||
set -e
|
||||
source venv/bin/activate
|
||||
|
||||
# Ensure a clean build and build_exe first
|
||||
python setup_cxfreeze.py build_exe
|
||||
# Ensure a clean build and build_exe first with -OO
|
||||
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)
|
||||
if [ -n "$build_dir" ] && [ -d "$build_dir/lib/assets/branding/screenshots" ]; then
|
||||
rm -rf "$build_dir/lib/assets/branding/screenshots"
|
||||
echo "Removed screenshots folder from $build_dir"
|
||||
|
||||
if [ -n "$build_dir" ]; then
|
||||
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
|
||||
|
||||
# 2. Remove unused Qt translations
|
||||
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
|
||||
python setup_cxfreeze.py bdist_appimage
|
||||
# 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
|
||||
version="${{ steps.get_version.outputs.VERSION }}"
|
||||
@@ -257,7 +358,7 @@ jobs:
|
||||
|
||||
# Install desktop file and icon
|
||||
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
|
||||
/opt/ytsage
|
||||
@@ -337,7 +438,7 @@ jobs:
|
||||
|
||||
# Desktop file and icon
|
||||
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
|
||||
cat > "$pkgroot/DEBIAN/control" <<CONTROL
|
||||
@@ -435,7 +536,7 @@ jobs:
|
||||
"cp -r dist/ytsage-v${version}-*/* /app/share/ytsage/",
|
||||
"ln -s /app/share/ytsage/ytsage /app/bin/ytsage",
|
||||
"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": [
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
venv
|
||||
~/.cache/pip
|
||||
~/Library/Caches/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
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
|
||||
|
||||
- name: Prepare build variables
|
||||
@@ -80,6 +80,13 @@ jobs:
|
||||
- name: Create cx_Freeze setup script
|
||||
shell: bash
|
||||
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'
|
||||
import os
|
||||
from cx_Freeze import setup, Executable
|
||||
@@ -88,21 +95,31 @@ jobs:
|
||||
|
||||
build_exe_options = dict(
|
||||
optimize=2,
|
||||
silent=True,
|
||||
packages=[
|
||||
"ytsage",
|
||||
"PySide6.QtCore",
|
||||
"PySide6.QtGui",
|
||||
"PySide6.QtWidgets",
|
||||
"PySide6.QtMultimedia",
|
||||
"PySide6.QtNetwork",
|
||||
"PySide6.QtDBus",
|
||||
"requests",
|
||||
"PIL",
|
||||
"packaging",
|
||||
"markdown",
|
||||
"pyglet",
|
||||
"loguru",
|
||||
"setuptools",
|
||||
],
|
||||
zip_include_packages=[
|
||||
"PySide6",
|
||||
"shiboken6",
|
||||
"requests",
|
||||
"PIL",
|
||||
"packaging",
|
||||
],
|
||||
excludes=[
|
||||
"PySide6.QtBluetooth",
|
||||
"PySide6.QtNetwork",
|
||||
"PySide6.QtOpenGL",
|
||||
"PySide6.QtPrintSupport",
|
||||
"PySide6.QtSvg",
|
||||
@@ -110,7 +127,6 @@ jobs:
|
||||
"PySide6.QtXml",
|
||||
"PySide6.QtSql",
|
||||
"PySide6.QtHelp",
|
||||
"PySide6.QtMultimedia",
|
||||
"PySide6.QtQml",
|
||||
"PySide6.QtQuick",
|
||||
"PySide6.QtWebEngineCore",
|
||||
@@ -125,21 +141,23 @@ jobs:
|
||||
"unittest",
|
||||
"test",
|
||||
"tests",
|
||||
"pydoc",
|
||||
"doctest",
|
||||
"email",
|
||||
],
|
||||
include_files=[
|
||||
("src", "src"),
|
||||
("assets/branding/icons", "lib/assets/branding/icons"),
|
||||
("assets/Icon", "lib/assets/Icon"),
|
||||
("assets/sound", "lib/assets/sound"),
|
||||
("languages", "lib/languages"),
|
||||
("ytsage/assets/Icon", "lib/assets/Icon"),
|
||||
("ytsage/assets/sound", "lib/assets/sound"),
|
||||
("ytsage/languages", "lib/languages"),
|
||||
("branding/icons", "lib/assets/branding/icons"),
|
||||
],
|
||||
)
|
||||
|
||||
executables = [
|
||||
Executable(
|
||||
script="main.py",
|
||||
script="ytsage_entry.py",
|
||||
target_name=f"YTSage-v{version}",
|
||||
icon="assets/branding/icons/icon.icns",
|
||||
icon="branding/icons/icon.icns",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -150,13 +168,12 @@ jobs:
|
||||
options={
|
||||
"build_exe": build_exe_options,
|
||||
"bdist_mac": {
|
||||
"iconfile": "assets/branding/icons/icon.icns",
|
||||
"iconfile": "branding/icons/icon.icns",
|
||||
"bundle_name": f"YTSage-v{version}",
|
||||
},
|
||||
"bdist_dmg": {
|
||||
"volume_label": f"YTSage v{version}",
|
||||
"applications_shortcut": True,
|
||||
# Sensible defaults; can be customized later if desired
|
||||
"format": "UDZO",
|
||||
"filesystem": "HFS+",
|
||||
"default_view": "icon-view",
|
||||
@@ -170,31 +187,79 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python setup_cxfreeze.py bdist_mac
|
||||
# Locate the built .app (cx_Freeze may place it under build/ or dist/)
|
||||
python -OO setup_cxfreeze.py bdist_mac
|
||||
|
||||
- name: Trim unnecessary files from App bundle
|
||||
shell: bash
|
||||
run: |
|
||||
version="${{ steps.get_version.outputs.VERSION }}"
|
||||
|
||||
# Locate the built .app
|
||||
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
|
||||
done
|
||||
if [ -z "$app_path" ]; then
|
||||
app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true)
|
||||
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)
|
||||
shell: bash
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python setup_cxfreeze.py bdist_dmg
|
||||
echo "Post-bdist_dmg directory listing:"
|
||||
echo "-- dist --"; ls -lah dist || true
|
||||
echo "-- build --"; ls -lah build || true
|
||||
if [ -n "$app_path" ]; then
|
||||
echo "Processing App Bundle at: $app_path"
|
||||
|
||||
# Simplified discovery of library directories
|
||||
# We search for 'lib' folders and then verify if they look like the python library folder
|
||||
find "$app_path/Contents" -type d -name "lib" | while read -r lib_dir; do
|
||||
# Check if this lib dir is a python library dir (contains asyncio or PySide6 or ytsage)
|
||||
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)
|
||||
shell: bash
|
||||
@@ -203,7 +268,7 @@ jobs:
|
||||
echo "Preparing artifacts for version: $version"
|
||||
mkdir -p artifacts
|
||||
|
||||
# Find the .app bundle (preferring versioned name)
|
||||
# Find the .app bundle
|
||||
app_path=""
|
||||
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
|
||||
@@ -211,35 +276,28 @@ jobs:
|
||||
if [ -z "$app_path" ]; then
|
||||
app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true)
|
||||
fi
|
||||
|
||||
if [ -n "$app_path" ] && [ -d "$app_path" ]; then
|
||||
app_base="$(basename "$app_path")"
|
||||
app_parent="$(dirname "$app_path")"
|
||||
# Ensure screenshots folder is not shipped
|
||||
if [ -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
|
||||
|
||||
# Create ZIP
|
||||
(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"
|
||||
else
|
||||
echo "Warning: .app bundle not found in dist/ or build/"
|
||||
fi
|
||||
|
||||
# Try to find the generated DMG in dist or build
|
||||
dmg_src=""
|
||||
for cand in "dist/YTSage-v${version}.dmg"; do
|
||||
if [ -f "$cand" ]; then dmg_src="$cand"; break; fi
|
||||
done
|
||||
if [ -z "$dmg_src" ]; then
|
||||
dmg_src=$(ls dist/*.dmg build/*.dmg build/dist/*.dmg 2>/dev/null | head -n1 || true)
|
||||
fi
|
||||
# Create DMG (Manual)
|
||||
echo "Creating DMG from $app_path..."
|
||||
mkdir -p dmg_stage
|
||||
cp -R "$app_path" "dmg_stage/"
|
||||
ln -s /Applications "dmg_stage/Applications"
|
||||
|
||||
if [ -n "$dmg_src" ] && [ -f "$dmg_src" ]; then
|
||||
# Rename the DMG to a consistent name in artifacts
|
||||
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"
|
||||
hdiutil create -volname "YTSage v${version}" -srcfolder "dmg_stage" -ov -format UDZO "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg"
|
||||
echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg"
|
||||
|
||||
rm -rf dmg_stage
|
||||
else
|
||||
echo "Warning: No DMG found in dist/ or build/"
|
||||
echo "Error: .app bundle not found in dist/ or build/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Final artifacts:"
|
||||
|
||||
@@ -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 }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
path: |
|
||||
venv
|
||||
~\AppData\Local\pip\Cache
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
python -m venv venv
|
||||
.\venv\Scripts\Activate.ps1
|
||||
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
|
||||
|
||||
- name: Prepare build variables
|
||||
@@ -72,6 +72,14 @@ jobs:
|
||||
- name: Create cx_Freeze setup script
|
||||
shell: powershell
|
||||
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
|
||||
New-Item -Path "setup_cxfreeze.py" -ItemType File -Force
|
||||
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 "build_exe_options = dict("
|
||||
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 ' "ytsage",'
|
||||
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.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 ' "PIL",'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' "packaging",'
|
||||
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 ' "setuptools",'
|
||||
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 ' "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.QtPrintSupport",'
|
||||
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.QtSql",'
|
||||
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.QtQuick",'
|
||||
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 ' "yt_dlp",'
|
||||
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 ' "tests",'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value " ],"
|
||||
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 ' ("assets/branding/icons", "lib/assets/branding/icons"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/Icon", "lib/assets/Icon"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/sound", "lib/assets/sound"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("languages", "lib/languages"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/assets/Icon", "lib/assets/Icon"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/assets/sound", "lib/assets/sound"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("ytsage/languages", "lib/languages"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("branding/icons", "lib/assets/branding/icons"),'
|
||||
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 " 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 ' 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 ""
|
||||
@@ -157,13 +175,77 @@ jobs:
|
||||
if (Test-Path "build\bdist.*") { Remove-Item "build\bdist.*" -Recurse -Force }
|
||||
if (Test-Path "dist") { Remove-Item "dist" -Recurse -Force }
|
||||
|
||||
# Build executable using setup script
|
||||
python setup_cxfreeze.py build_exe --build-exe "dist\YTSage"
|
||||
# Build executable using setup script with extra optimization
|
||||
python -OO setup_cxfreeze.py build_exe --build-exe "dist\YTSage"
|
||||
|
||||
# Remove screenshots folder to reduce build size
|
||||
if (Test-Path "dist\YTSage\lib\assets\branding\screenshots") {
|
||||
Remove-Item "dist\YTSage\lib\assets\branding\screenshots" -Recurse -Force
|
||||
Write-Host "Removed screenshots folder from standard build"
|
||||
- name: Trim unnecessary files from Standard build
|
||||
shell: powershell
|
||||
run: |
|
||||
$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
|
||||
@@ -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"
|
||||
|
||||
# Build executable with FFmpeg using setup script
|
||||
python 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"
|
||||
}
|
||||
python -OO setup_cxfreeze_ffmpeg.py build_exe --build-exe "dist\YTSage-FFmpeg"
|
||||
|
||||
# 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
|
||||
@@ -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)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -303,6 +449,39 @@ jobs:
|
||||
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
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
@@ -29,3 +29,9 @@ jobs:
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
secrets: inherit
|
||||
|
||||
release-pypi:
|
||||
uses: ./.github/workflows/build-pypi.yml
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
include pyproject.toml
|
||||
recursive-include ytsage/assets *
|
||||
recursive-include ytsage/languages *
|
||||
@@ -1,13 +1,16 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="assets\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\svg\ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
|
||||
<img src="branding\screenshots\main.png" width="800" alt="YTSage Interface"/>
|
||||
|
||||
[](https://badge.fury.io/py/ytsage)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://pepy.tech/project/ytsage)
|
||||
[](https://github.com/oop7/YTSage/stargazers)
|
||||
[](https://pepy.tech/project/ytsage)
|
||||
[](https://github.com/oop7/YTSage/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/oop7/YTSage/releases)
|
||||
[](https://github.com/oop7/YTSage/stargazers)
|
||||
[](https://pypi.org/project/ytsage/)
|
||||
[](https://github.com/sponsors/oop7)
|
||||
|
||||
**A modern YouTube downloader with a clean PySide6 interface.**
|
||||
Download videos in any quality, extract audio, fetch subtitles, and more.
|
||||
@@ -63,6 +66,15 @@ Install YTSage from PyPI:
|
||||
pip install ytsage
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🔄 Update an existing installation</summary>
|
||||
|
||||
```bash
|
||||
pip install --upgrade ytsage
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Then launch the app:
|
||||
|
||||
```bash
|
||||
@@ -71,6 +83,8 @@ ytsage
|
||||
|
||||
### 📦 Pre-built Executables
|
||||
|
||||
> [👉 Download Latest Release](https://github.com/oop7/YTSage/releases/latest)
|
||||
|
||||
#### 🪟 Windows
|
||||
|
||||
| Format | Description |
|
||||
@@ -80,6 +94,14 @@ ytsage
|
||||
|  | Portable version, no installation required |
|
||||
|  | 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
|
||||
|
||||
| Format | Description |
|
||||
@@ -87,6 +109,30 @@ ytsage
|
||||
|  | Debian package |
|
||||
|  | AppImage, portable |
|
||||
|  | RPM package |
|
||||
|  | 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
|
||||
|
||||
@@ -95,10 +141,19 @@ ytsage
|
||||
|  | Zipped application for Apple Silicon |
|
||||
|  | 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>
|
||||
<summary>🛠️ Manual Installation from Source</summary>
|
||||
<summary>💻 Manual Installation from Source</summary>
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
@@ -112,19 +167,19 @@ cd YTSage
|
||||
#### ⚡ With uv
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
uv pip install .
|
||||
```
|
||||
|
||||
#### 📦 Or with standard pip
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
```
|
||||
|
||||
### 3. Run the Application
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
python -m ytsage.main
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -135,16 +190,16 @@ python main.py
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="assets\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\Download-Settings.png" alt="Download Settings" width="400"/></td>
|
||||
<td><img src="branding\screenshots\playlist.png" alt="Playlist Download" width="400"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><em>Download Settings</em></td>
|
||||
<td align="center"><em>Playlist Download</em></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="assets\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\audio_format.png" alt="Audio Format Selection with Save Thumbnail" width="400"/></td>
|
||||
<td><img src="branding\screenshots\Custom-Option.png" alt="Custom Options" width="400"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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
|
||||
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**.
|
||||
- **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:
|
||||
- **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
|
||||
@@ -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.
|
||||
- **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>`)
|
||||
- **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**.
|
||||
- **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>
|
||||
|
||||
@@ -342,47 +398,23 @@ YTSage/
|
||||
│ │ │── build-windows.yml # Windows build workflow
|
||||
| | └── release-all.yml # Master release workflow
|
||||
│ └── 📄 CI_CD_README.md # CI/CD documentation
|
||||
├── 📁 assets/ # Static assets and resources
|
||||
│ ├── 📁 branding/ # Branding assets
|
||||
│ │ ├── 📁 icons/ # Application icons
|
||||
│ │ │ ├── icon.icns # macOS icon
|
||||
│ │ │ ├── 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
|
||||
├── 📁 branding/ # Branding assets (Screenshots, SVGs)
|
||||
│ ├── 📁 icons/ # Application icons
|
||||
│ ├── 📁 screenshots/ # Screenshots for documentation
|
||||
│ └── 📁 svg/ # SVG assets
|
||||
├── 📄 LICENSE # License file
|
||||
├── 📄 main.py # Application entry point
|
||||
├── 📄 pyproject.toml # Project metadata and dependencies
|
||||
├── 📄 README.md # Project documentation
|
||||
├── 📄 .gitignore # Git ignore rules
|
||||
├── 📄 requirements.txt # Python dependencies
|
||||
└── 📁 src/ # Source code
|
||||
|
|
||||
├── 📄 requirements.txt # Python dependencies (dev)
|
||||
└── 📁 ytsage/ # Source package
|
||||
├── 📁 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
|
||||
│ ├── 📄 __init__.py # Core package init
|
||||
│ ├── 📄 ytsage_deno.py # Deno integration
|
||||
@@ -392,26 +424,14 @@ YTSage/
|
||||
│ └── 📄 ytsage_yt_dlp.py # yt-dlp integration
|
||||
├── 📁 gui/ # User interface components
|
||||
│ ├── 📄 __init__.py # GUI package init
|
||||
│ ├── 📄 ytsage_gui_format_table.py # Format table functionality
|
||||
│ ├── 📄 ytsage_gui_main.py # Main application window
|
||||
│ ├── 📄 ytsage_gui_video_info.py # Video information display
|
||||
│ └── 📁 ytsage_gui_dialogs/ # Dialog classes
|
||||
│ ├── 📄 __init__.py # Dialogs package init
|
||||
│ ├── 📄 ytsage_dialogs_base.py # Basic dialogs
|
||||
│ ├── 📄 ytsage_dialogs_custom.py # Custom functionality dialogs
|
||||
│ ├── 📄 ytsage_dialogs_ffmpeg.py # FFmpeg-related dialogs
|
||||
│ ├── 📄 ytsage_dialogs_history.py # History dialogs
|
||||
│ ├── 📄 ytsage_dialogs_selection.py # Selection dialogs
|
||||
│ ├── 📄 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
|
||||
├── 📁 utils/ # Utility modules
|
||||
│ ├── 📄 __init__.py # Utils package init
|
||||
│ ├── 📄 ytsage_config_manager.py # Configuration management
|
||||
│ └── 📄 ytsage_logger.py # Logging utilities
|
||||
├── 📄 __init__.py # Package entry point
|
||||
└── 📄 main.py # Main execution script
|
||||
```
|
||||
|
||||
</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>Markdown Rendering</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://pyglet.org/">pyglet</a></td>
|
||||
<td>Audio Playback</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/Delgan/loguru">loguru</a></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 |
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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?
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
@@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader
|
||||
A modern, user-friendly YouTube video downloader built with PySide6.
|
||||
"""
|
||||
|
||||
__version__ = "4.9.7"
|
||||
__version__ = "5.0.0b5"
|
||||
__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,
|
||||
)
|
||||
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_constants import (
|
||||
from ..utils.ytsage_logger import logger
|
||||
from ..utils.ytsage_localization import _
|
||||
from ..utils.ytsage_constants import (
|
||||
APP_BIN_DIR,
|
||||
ICON_PATH,
|
||||
OS_FULL_NAME,
|
||||
@@ -31,7 +31,7 @@ from src.utils.ytsage_constants import (
|
||||
DENO_DOWNLOAD_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:
|
||||
@@ -171,8 +171,9 @@ class DownloadDenoThread(QThread):
|
||||
self.finished_signal.emit(False, f"Executable '{executable_name}' not found in zip")
|
||||
return
|
||||
|
||||
# Extract to app bin directory
|
||||
zip_ref.extract(executable_name, APP_BIN_DIR)
|
||||
# Extract to app bin directory (while zip_ref is open)
|
||||
target_dir = DENO_APP_BIN_PATH.parent
|
||||
zip_ref.extract(executable_name, target_dir)
|
||||
|
||||
# Verify the extracted file exists
|
||||
exe_path = DENO_APP_BIN_PATH
|
||||
@@ -682,10 +683,13 @@ def compare_deno_versions(current: str, latest: str) -> bool:
|
||||
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.
|
||||
|
||||
Args:
|
||||
progress_callback: Optional function to call with output lines for progress tracking
|
||||
|
||||
Returns:
|
||||
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}")
|
||||
|
||||
# Run deno upgrade command
|
||||
result = subprocess.run(
|
||||
# Run deno upgrade command with output capturing
|
||||
process = subprocess.Popen(
|
||||
[str(deno_path), "upgrade"],
|
||||
capture_output=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
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.debug(f"Upgrade output: {output}")
|
||||
return True, output
|
||||
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}")
|
||||
return False, output
|
||||
|
||||
@@ -11,10 +11,16 @@ from typing import Optional, List, Set
|
||||
|
||||
from PySide6.QtCore import QObject, QThread, Signal
|
||||
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS
|
||||
from src.utils.ytsage_localization import LocalizationManager
|
||||
from src.utils.ytsage_logger import logger
|
||||
from .ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ..utils.ytsage_constants import (
|
||||
SUBPROCESS_CREATIONFLAGS,
|
||||
VIDEO_EXTENSIONS,
|
||||
AUDIO_EXTENSIONS,
|
||||
SUBTITLE_EXTENSIONS,
|
||||
MEDIA_EXTENSIONS,
|
||||
)
|
||||
from ..utils.ytsage_localization import LocalizationManager
|
||||
from ..utils.ytsage_logger import logger
|
||||
|
||||
# Shorthand for localization
|
||||
_ = LocalizationManager.get_text
|
||||
@@ -66,6 +72,7 @@ class DownloadThread(QThread):
|
||||
preferred_output_format="mp4",
|
||||
force_audio_format=False,
|
||||
preferred_audio_format="best",
|
||||
filename_format=None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.url = url
|
||||
@@ -93,6 +100,7 @@ class DownloadThread(QThread):
|
||||
self.preferred_output_format = preferred_output_format
|
||||
self.force_audio_format = force_audio_format
|
||||
self.preferred_audio_format = preferred_audio_format
|
||||
self.filename_format = filename_format
|
||||
self.paused: bool = False
|
||||
self.cancelled: bool = False
|
||||
self.process: Optional[subprocess.Popen] = None
|
||||
@@ -189,9 +197,12 @@ class DownloadThread(QThread):
|
||||
|
||||
def safe_delete(path: Path) -> bool:
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
logger.debug(f"Deleted subtitle file: {path.name}")
|
||||
return True
|
||||
# Check if file exists before trying to delete
|
||||
if path.exists():
|
||||
path.unlink(missing_ok=True)
|
||||
logger.debug(f"Deleted subtitle file: {path.name}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.exception(f"Error deleting subtitle file {path}: {e}")
|
||||
return False
|
||||
@@ -265,11 +276,14 @@ class DownloadThread(QThread):
|
||||
# Use string concatenation instead of Path.joinpath to avoid Path object issues
|
||||
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:
|
||||
# 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:
|
||||
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)])
|
||||
|
||||
@@ -376,7 +390,9 @@ class DownloadThread(QThread):
|
||||
def _run_direct_command(self) -> None:
|
||||
"""Run yt-dlp as a direct command line process instead of using Python API."""
|
||||
try:
|
||||
self.error_lines = [] # Initialize error capture list
|
||||
cmd: List[str] = self._build_yt_dlp_command()
|
||||
|
||||
cmd_str: str = " ".join(shlex.quote(str(arg)) for arg in cmd)
|
||||
logger.debug(f"Executing command: {cmd_str}")
|
||||
|
||||
@@ -410,8 +426,6 @@ class DownloadThread(QThread):
|
||||
self._terminate_process_tree(self.process)
|
||||
|
||||
# Add delay before cleanup to allow file handles to be released
|
||||
# Force garbage collection to help release resources
|
||||
gc.collect()
|
||||
time.sleep(2)
|
||||
self.cleanup_partial_files()
|
||||
self.status_signal.emit(_("download.cancelled"))
|
||||
@@ -443,10 +457,6 @@ class DownloadThread(QThread):
|
||||
final_file_found = False
|
||||
|
||||
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
|
||||
if self.last_file_path:
|
||||
last_path = Path(self.last_file_path)
|
||||
@@ -462,7 +472,7 @@ class DownloadThread(QThread):
|
||||
potential_files = []
|
||||
|
||||
# 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}'))
|
||||
# Also check subdirectories (for playlist downloads)
|
||||
potential_files.extend(self.path.glob(f'*/*{ext}'))
|
||||
@@ -505,13 +515,18 @@ class DownloadThread(QThread):
|
||||
if self.cancelled:
|
||||
self.status_signal.emit(_("download.cancelled"))
|
||||
else:
|
||||
# Provide more descriptive error message for possible yt-dlp conflicts
|
||||
if return_code == 1:
|
||||
# Provide informative error message based on captured output
|
||||
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(
|
||||
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:
|
||||
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
|
||||
time.sleep(1)
|
||||
@@ -519,7 +534,7 @@ class DownloadThread(QThread):
|
||||
|
||||
except Exception as 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
|
||||
time.sleep(1)
|
||||
self.cleanup_partial_files()
|
||||
@@ -529,6 +544,11 @@ class DownloadThread(QThread):
|
||||
line = line.strip()
|
||||
# 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
|
||||
# Use a slightly more robust regex looking for the start of the 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:
|
||||
self.status_signal.emit(_("download.downloading_audio"))
|
||||
# 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"))
|
||||
# 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"))
|
||||
# Subtitle file extensions
|
||||
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
|
||||
elif ext in SUBTITLE_EXTENSIONS:
|
||||
self.status_signal.emit(_("download.downloading_subtitle"))
|
||||
# Default case
|
||||
else:
|
||||
@@ -622,6 +642,14 @@ class DownloadThread(QThread):
|
||||
if "Downloading webpage" in line or "Extracting URL" in line:
|
||||
self.status_signal.emit(_("download.fetching_info"))
|
||||
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:
|
||||
self.status_signal.emit(_("download.processing_playlist"))
|
||||
self.progress_signal.emit(0)
|
||||
@@ -695,11 +723,11 @@ class DownloadThread(QThread):
|
||||
# Determine file type based on extension for existing file message
|
||||
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")
|
||||
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
|
||||
elif ext in AUDIO_EXTENSIONS:
|
||||
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")
|
||||
else:
|
||||
self.status_signal.emit(f"⚠️ File already exists")
|
||||
@@ -716,13 +744,13 @@ class DownloadThread(QThread):
|
||||
ext = Path(self.current_filename).suffix.lower()
|
||||
|
||||
# Video file extensions
|
||||
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
|
||||
if ext in VIDEO_EXTENSIONS:
|
||||
self.status_signal.emit(_("download.video_completed"))
|
||||
# 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"))
|
||||
# Subtitle file extensions
|
||||
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
|
||||
elif ext in SUBTITLE_EXTENSIONS:
|
||||
self.status_signal.emit(_("download.subtitle_completed"))
|
||||
# Default case
|
||||
else:
|
||||
@@ -7,8 +7,8 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
from ..utils.ytsage_logger import logger
|
||||
from ..utils.ytsage_constants import (
|
||||
FFMPEG_7Z_DOWNLOAD_URL,
|
||||
FFMPEG_7Z_SHA256_URL,
|
||||
FFMPEG_ZIP_DOWNLOAD_URL,
|
||||
@@ -11,9 +11,9 @@ from typing import Any, Dict, Optional, Union
|
||||
import requests
|
||||
from packaging import version
|
||||
|
||||
from src.core.ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import (
|
||||
from .ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
|
||||
from .ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ..utils.ytsage_constants import (
|
||||
APP_CONFIG_FILE,
|
||||
OS_NAME,
|
||||
SUBPROCESS_CREATIONFLAGS,
|
||||
@@ -21,8 +21,8 @@ from src.utils.ytsage_constants import (
|
||||
YTDLP_APP_BIN_PATH,
|
||||
YTDLP_DOWNLOAD_URL,
|
||||
)
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_logger import logger
|
||||
from ..utils.ytsage_localization import _
|
||||
from ..utils.ytsage_logger import logger
|
||||
|
||||
try:
|
||||
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:
|
||||
"""Load cached version info from config file."""
|
||||
from ..utils.ytsage_config_manager import ConfigManager
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
cached_versions = config.get("cached_versions", {})
|
||||
cached_versions = ConfigManager.get("cached_versions") or {}
|
||||
|
||||
for tool_name, cache_data in cached_versions.items():
|
||||
if tool_name in _version_cache:
|
||||
@@ -120,10 +121,10 @@ def load_version_cache_from_config() -> None:
|
||||
|
||||
def save_version_cache_to_config() -> None:
|
||||
"""Save version cache to config file."""
|
||||
from ..utils.ytsage_config_manager import ConfigManager
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
config["cached_versions"] = _version_cache.copy()
|
||||
save_config(config)
|
||||
ConfigManager.set("cached_versions", _version_cache.copy())
|
||||
except Exception as 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:
|
||||
"""Get Deno version with caching support."""
|
||||
try:
|
||||
from src.core.ytsage_deno import get_deno_path
|
||||
from .ytsage_deno import get_deno_path
|
||||
|
||||
current_path = get_deno_path()
|
||||
|
||||
@@ -189,7 +190,7 @@ def get_deno_version_cached() -> str:
|
||||
return cached_version
|
||||
|
||||
# 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)
|
||||
|
||||
# Update cache
|
||||
@@ -214,7 +215,7 @@ def refresh_version_cache(force=False) -> bool:
|
||||
update_version_cache("ffmpeg", version_info, "ffmpeg", force_save=True)
|
||||
|
||||
# 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()
|
||||
version_info = get_deno_version_direct(deno_path)
|
||||
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_config_file_path() moved to src\utils\ytsage_constants.py
|
||||
# ensure_app_data_dir() moved to src\utils\ytsage_constants.py
|
||||
|
||||
|
||||
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
|
||||
# load_config() and save_config() removed - use ConfigManager instead
|
||||
|
||||
|
||||
def check_ffmpeg() -> bool:
|
||||
@@ -593,15 +550,15 @@ def update_yt_dlp() -> bool:
|
||||
|
||||
def should_check_for_auto_update() -> bool:
|
||||
"""Check if auto-update should be performed based on user settings."""
|
||||
try:
|
||||
config = load_config()
|
||||
from ..utils.ytsage_config_manager import ConfigManager
|
||||
|
||||
try:
|
||||
# Check if auto-update is enabled
|
||||
if not config.get("auto_update_ytdlp", False):
|
||||
if not ConfigManager.get("auto_update_ytdlp"):
|
||||
return False
|
||||
|
||||
frequency: str = config.get("auto_update_frequency", "daily")
|
||||
last_check: float = config.get("last_update_check", 0)
|
||||
frequency: str = ConfigManager.get("auto_update_frequency") or "daily"
|
||||
last_check: float = ConfigManager.get("last_update_check") or 0
|
||||
current_time: float = time.time()
|
||||
|
||||
# Calculate time since last check
|
||||
@@ -623,6 +580,8 @@ def should_check_for_auto_update() -> bool:
|
||||
|
||||
def check_and_update_ytdlp_auto() -> bool:
|
||||
"""Perform automatic yt-dlp update check and update if needed."""
|
||||
from ..utils.ytsage_config_manager import ConfigManager
|
||||
|
||||
try:
|
||||
logger.info("Performing automatic yt-dlp update check...")
|
||||
|
||||
@@ -653,9 +612,7 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
if update_yt_dlp():
|
||||
logger.info("Auto-update completed successfully!")
|
||||
# Update the last check timestamp
|
||||
config = load_config()
|
||||
config["last_update_check"] = time.time()
|
||||
save_config(config)
|
||||
ConfigManager.set("last_update_check", time.time())
|
||||
return True
|
||||
else:
|
||||
logger.info("Auto-update failed")
|
||||
@@ -663,9 +620,7 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
else:
|
||||
logger.info("yt-dlp is already up to date")
|
||||
# Still update the timestamp even if no update was needed
|
||||
config = load_config()
|
||||
config["last_update_check"] = time.time()
|
||||
save_config(config)
|
||||
ConfigManager.set("last_update_check", time.time())
|
||||
return True
|
||||
|
||||
except requests.RequestException as e:
|
||||
@@ -682,7 +637,7 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
|
||||
def get_auto_update_settings() -> Dict[str, Any]:
|
||||
"""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")
|
||||
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:
|
||||
"""Update auto-update settings in config."""
|
||||
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_frequency", frequency)
|
||||
@@ -20,8 +20,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
from ..utils.ytsage_logger import logger
|
||||
from ..utils.ytsage_constants import (
|
||||
APP_BIN_DIR,
|
||||
ICON_PATH,
|
||||
OS_FULL_NAME,
|
||||
@@ -31,7 +31,8 @@ from src.utils.ytsage_constants import (
|
||||
YTDLP_DOWNLOAD_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
|
||||
# get_ytdlp_install_dir() moved to src\utils\ytsage_constants.py
|
||||
@@ -162,7 +163,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("yt-dlp Setup Required")
|
||||
self.setWindowTitle(_("ytdlp_setup.required_title"))
|
||||
self.setMinimumWidth(520)
|
||||
self.setMinimumHeight(350)
|
||||
self.resize(520, 380)
|
||||
@@ -253,7 +254,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
layout.setContentsMargins(25, 25, 25, 25)
|
||||
|
||||
# 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.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title_label)
|
||||
@@ -262,10 +263,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
# os_name logic moved to src\utils\ytsage_constants.py
|
||||
|
||||
info_label = QLabel(
|
||||
f"YTSage requires yt-dlp to download videos.<br><br>"
|
||||
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:"
|
||||
_("ytdlp_setup.description", os_name=OS_FULL_NAME)
|
||||
)
|
||||
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
info_label.setWordWrap(True)
|
||||
@@ -278,9 +276,9 @@ class YtdlpSetupDialog(QDialog):
|
||||
option_layout.setSpacing(8)
|
||||
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.manual_radio = QRadioButton("Select path manually")
|
||||
self.manual_radio = QRadioButton(_("ytdlp_setup.option_manual"))
|
||||
|
||||
option_layout.addWidget(self.auto_radio)
|
||||
option_layout.addWidget(self.manual_radio)
|
||||
@@ -326,10 +324,10 @@ class YtdlpSetupDialog(QDialog):
|
||||
button_layout.setSpacing(15)
|
||||
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.cancel_button = QPushButton("Cancel")
|
||||
self.cancel_button = QPushButton(_("buttons.cancel"))
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addWidget(self.setup_button)
|
||||
@@ -347,7 +345,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
def download_ytdlp(self) -> None:
|
||||
self.progress_bar.setVisible(True)
|
||||
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.cancel_button.setEnabled(False)
|
||||
|
||||
@@ -364,15 +362,15 @@ class YtdlpSetupDialog(QDialog):
|
||||
self.cancel_button.setEnabled(True)
|
||||
|
||||
if success:
|
||||
self.status_label.setText("yt-dlp was successfully installed!")
|
||||
self.status_label.setText(_("ytdlp_setup.success"))
|
||||
self.setup_complete.emit(result)
|
||||
self.accept()
|
||||
else:
|
||||
self.status_label.setText(f"Error: {result}")
|
||||
self.status_label.setText(_("ytdlp_setup.error", error=result))
|
||||
error_dialog = QMessageBox(self)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
error_dialog.setWindowTitle("Download Failed")
|
||||
error_dialog.setText(f"Failed to download yt-dlp: {result}")
|
||||
error_dialog.setWindowTitle(_("ytdlp_setup.download_failed_title"))
|
||||
error_dialog.setText(_("ytdlp_setup.download_failed_message", error=result))
|
||||
# Set the window icon to match the main dialog
|
||||
error_dialog.setWindowIcon(self.windowIcon())
|
||||
error_dialog.setStyleSheet(
|
||||
@@ -401,9 +399,9 @@ class YtdlpSetupDialog(QDialog):
|
||||
|
||||
def select_ytdlp_path(self) -> None:
|
||||
if OS_NAME == "Windows":
|
||||
file_filter = "Executable Files (*.exe)"
|
||||
file_filter = _("ytdlp_setup.file_filter_windows")
|
||||
else:
|
||||
file_filter = "All Files (*)"
|
||||
file_filter = _("ytdlp_setup.file_filter_all")
|
||||
|
||||
# Apply style to QFileDialog
|
||||
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:
|
||||
logger.debug(f"User selected file: {file_path}")
|
||||
@@ -465,7 +465,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
logger.debug(f"Permissions set on Unix system")
|
||||
|
||||
# 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}")
|
||||
self.setup_complete.emit(target_path)
|
||||
self.accept()
|
||||
@@ -473,8 +473,8 @@ class YtdlpSetupDialog(QDialog):
|
||||
logger.debug(f"Error copying file: {copy_error}", exc_info=True)
|
||||
error_dialog = QMessageBox(self)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
error_dialog.setWindowTitle("Setup Error")
|
||||
error_dialog.setText(f"Error copying yt-dlp to app directory: {copy_error}")
|
||||
error_dialog.setWindowTitle(_("ytdlp_setup.setup_error_title"))
|
||||
error_dialog.setText(_("ytdlp_setup.copy_error", error=copy_error))
|
||||
error_dialog.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -502,8 +502,8 @@ class YtdlpSetupDialog(QDialog):
|
||||
logger.debug(f"File verification failed with return code: {result.returncode}")
|
||||
error_dialog = QMessageBox(self)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Warning)
|
||||
error_dialog.setWindowTitle("Invalid Executable")
|
||||
error_dialog.setText("The selected file does not appear to be a valid yt-dlp executable.")
|
||||
error_dialog.setWindowTitle(_("ytdlp_setup.invalid_executable_title"))
|
||||
error_dialog.setText(_("ytdlp_setup.invalid_executable_message"))
|
||||
error_dialog.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -531,8 +531,8 @@ class YtdlpSetupDialog(QDialog):
|
||||
logger.debug(f"Exception during verification: {e}", exc_info=True)
|
||||
error_dialog = QMessageBox(self)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
error_dialog.setWindowTitle("Error")
|
||||
error_dialog.setText(f"Error verifying yt-dlp executable: {e}")
|
||||
error_dialog.setWindowTitle(_("main_ui.error_title"))
|
||||
error_dialog.setText(_("ytdlp_setup.verify_error", error=e))
|
||||
error_dialog.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -678,8 +678,8 @@ def setup_ytdlp(parent_widget=None):
|
||||
if parent_widget:
|
||||
error_dialog = QMessageBox(parent_widget)
|
||||
error_dialog.setIcon(QMessageBox.Icon.Warning)
|
||||
error_dialog.setWindowTitle("Setup Failed")
|
||||
error_dialog.setText("Failed to set up yt-dlp. Some features may not work correctly.")
|
||||
error_dialog.setWindowTitle(_("ytdlp_setup.setup_failed_title"))
|
||||
error_dialog.setText(_("ytdlp_setup.setup_failed_message"))
|
||||
# Set the window icon to match the parent
|
||||
error_dialog.setWindowIcon(parent_widget.windowIcon())
|
||||
error_dialog.setStyleSheet(
|
||||
@@ -712,3 +712,43 @@ def setup_ytdlp(parent_widget=None):
|
||||
# User cancelled or setup failed, return the fallback command
|
||||
logger.debug("Returning fallback command '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
|
||||
@@ -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
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_history import HistoryDialog
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import (
|
||||
from .ytsage_dialogs_base import AboutDialog, LogWindow
|
||||
from .ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog
|
||||
from .ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread
|
||||
from .ytsage_dialogs_history import HistoryDialog
|
||||
from .ytsage_dialogs_selection import (
|
||||
PlaylistSelectionDialog,
|
||||
SponsorBlockCategoryDialog,
|
||||
SubtitleSelectionDialog,
|
||||
)
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget
|
||||
from .ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog
|
||||
from .ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog
|
||||
from .ytsage_dialogs_updater import UpdaterTabWidget
|
||||
|
||||
__all__ = [
|
||||
# Base dialogs
|
||||
@@ -5,7 +5,8 @@ Contains basic utility dialogs like LogWindow and AboutDialog.
|
||||
|
||||
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 (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
@@ -19,19 +20,66 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src import __version__ as APP_VERSION
|
||||
from src.utils.ytsage_localization import _
|
||||
from ... import __version__ as APP_VERSION
|
||||
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 src.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 src.core.ytsage_deno import check_deno_installed, get_deno_path
|
||||
from ...core.ytsage_ffmpeg import get_ffmpeg_path
|
||||
from ...core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, get_deno_version, refresh_version_cache
|
||||
from ...core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path, check_ytdlp_deno_integration
|
||||
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):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("yt-dlp Log")
|
||||
self.setWindowTitle(_("dialogs.ytdlp_log_title"))
|
||||
self.setMinimumSize(700, 500)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
@@ -178,18 +226,25 @@ class AboutDialog(QDialog):
|
||||
info_layout = QHBoxLayout()
|
||||
info_layout.setSpacing(15)
|
||||
|
||||
author_link = '<a href="https://github.com/oop7/" style="color: #c90000; text-decoration: none;">oop7</a>'
|
||||
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)
|
||||
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(
|
||||
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)
|
||||
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
|
||||
info_container = QHBoxLayout()
|
||||
info_container.addStretch()
|
||||
@@ -241,6 +296,34 @@ class AboutDialog(QDialog):
|
||||
# Add stretch to push refresh button to the right
|
||||
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
|
||||
self.refresh_btn = QPushButton(_("about.refresh"))
|
||||
self.refresh_btn.setFixedSize(16, 16)
|
||||
@@ -288,14 +371,15 @@ class AboutDialog(QDialog):
|
||||
|
||||
def _show_loading_message(self) -> None:
|
||||
"""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.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
color: #cccccc;
|
||||
color: #888888;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
padding: 10px;
|
||||
}
|
||||
"""
|
||||
@@ -376,7 +460,13 @@ class AboutDialog(QDialog):
|
||||
return item_widget
|
||||
|
||||
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
|
||||
for i in reversed(range(self.status_container.count())):
|
||||
child = self.status_container.itemAt(i).widget()
|
||||
@@ -384,19 +474,18 @@ class AboutDialog(QDialog):
|
||||
child.deleteLater()
|
||||
|
||||
# yt-dlp Status - compact version with path
|
||||
ytdlp_found = check_ytdlp_installed()
|
||||
ytdlp_found = info['ytdlp_found']
|
||||
ytdlp_status_text = (
|
||||
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
|
||||
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
|
||||
|
||||
# Simplified cache status
|
||||
ytdlp_cache = _version_cache.get("ytdlp", {})
|
||||
last_check = ytdlp_cache.get("last_check", 0)
|
||||
last_check = info['ytdlp_last_check']
|
||||
cache_status = ""
|
||||
if last_check > 0:
|
||||
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
||||
@@ -412,23 +501,19 @@ class AboutDialog(QDialog):
|
||||
self.status_container.addWidget(ytdlp_item)
|
||||
|
||||
# FFmpeg Status - compact version with path
|
||||
ffmpeg_found = check_ffmpeg()
|
||||
ffmpeg_found = info['ffmpeg_found']
|
||||
ffmpeg_status_text = (
|
||||
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
|
||||
if ffmpeg_found
|
||||
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
|
||||
ffmpeg_path_text = None
|
||||
if ffmpeg_found:
|
||||
ffmpeg_path = get_ffmpeg_path()
|
||||
ffmpeg_path_text = ffmpeg_path if ffmpeg_path else None
|
||||
ffmpeg_path_text = info['ffmpeg_path']
|
||||
|
||||
# Simplified cache status for FFmpeg
|
||||
ffmpeg_cache = _version_cache.get("ffmpeg", {})
|
||||
last_check = ffmpeg_cache.get("last_check", 0)
|
||||
last_check = info['ffmpeg_last_check']
|
||||
cache_status = ""
|
||||
if last_check > 0 and ffmpeg_found:
|
||||
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
||||
@@ -444,41 +529,60 @@ class AboutDialog(QDialog):
|
||||
self.status_container.addWidget(ffmpeg_item)
|
||||
|
||||
# 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 = (
|
||||
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
|
||||
deno_path_text = None
|
||||
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
|
||||
if deno_path and deno_path != "deno":
|
||||
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
|
||||
if Path(deno_path).resolve() == DENO_APP_BIN_PATH.resolve():
|
||||
deno_path_text = deno_path
|
||||
|
||||
# Simplified cache status for Deno
|
||||
deno_cache = _version_cache.get("deno", {})
|
||||
last_check = deno_cache.get("last_check", 0)
|
||||
last_check = info['deno_last_check']
|
||||
cache_status = ""
|
||||
if last_check > 0 and deno_found:
|
||||
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
||||
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",
|
||||
deno_status_text,
|
||||
deno_version + cache_status,
|
||||
deno_version + cache_status + integration_status,
|
||||
deno_path_text,
|
||||
)
|
||||
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:
|
||||
"""Refresh version information manually."""
|
||||
self.refresh_btn.setText(_('about.refreshing'))
|
||||
@@ -29,16 +29,17 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.core.ytsage_utils import update_auto_update_settings
|
||||
from src.utils.ytsage_constants import YTDLP_DOCS_URL
|
||||
from src.utils.ytsage_config_manager import ConfigManager
|
||||
from src.utils.ytsage_localization import LocalizationManager, _
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget
|
||||
from ..ytsage_smooth_tab_widget import SmoothTabWidget
|
||||
from ...core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ...core.ytsage_utils import update_auto_update_settings
|
||||
from ...utils.ytsage_constants import YTDLP_DOCS_URL
|
||||
from ...utils.ytsage_config_manager import ConfigManager
|
||||
from ...utils.ytsage_localization import LocalizationManager, _
|
||||
from ...utils.ytsage_logger import logger
|
||||
from .ytsage_dialogs_updater import UpdaterTabWidget
|
||||
|
||||
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):
|
||||
@@ -115,7 +116,7 @@ class CustomOptionsDialog(QDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Create tab widget to organize content
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget = SmoothTabWidget()
|
||||
layout.addWidget(self.tab_widget)
|
||||
|
||||
# === Cookies Tab ===
|
||||
@@ -542,7 +543,7 @@ class CustomOptionsDialog(QDialog):
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
QFrame#tabContent {
|
||||
border: 1px solid #3d3d3d;
|
||||
background-color: #15181b;
|
||||
}
|
||||
@@ -675,13 +676,17 @@ class CustomOptionsDialog(QDialog):
|
||||
def _update_cookies_active_status(self) -> None:
|
||||
"""Update the status indicator showing if cookies are currently active"""
|
||||
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;")
|
||||
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;")
|
||||
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;")
|
||||
|
||||
def _initialize_proxy_settings(self) -> None:
|
||||
@@ -854,9 +859,9 @@ class CustomOptionsDialog(QDialog):
|
||||
if saved_main or saved_geo:
|
||||
status_parts = []
|
||||
if saved_main:
|
||||
status_parts.append(f"Saved main proxy: {saved_main}")
|
||||
status_parts.append(_("proxy.saved_main", proxy=saved_main))
|
||||
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.setStyleSheet("color: #888888; font-style: italic;")
|
||||
else:
|
||||
@@ -866,10 +871,10 @@ class CustomOptionsDialog(QDialog):
|
||||
issues = []
|
||||
|
||||
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):
|
||||
issues.append("Invalid geo proxy URL format")
|
||||
issues.append(_("proxy.invalid_geo_url"))
|
||||
|
||||
if issues:
|
||||
self.proxy_status.setText(" | ".join(issues))
|
||||
@@ -877,9 +882,9 @@ class CustomOptionsDialog(QDialog):
|
||||
else:
|
||||
status_parts = []
|
||||
if main_proxy:
|
||||
status_parts.append("Main proxy configured")
|
||||
status_parts.append(_("proxy.main_configured"))
|
||||
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.setStyleSheet("color: #00cc00; font-style: italic;")
|
||||
@@ -887,24 +892,24 @@ class CustomOptionsDialog(QDialog):
|
||||
def run_custom_command(self) -> None:
|
||||
url = self._parent.url_input.text().strip()
|
||||
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
|
||||
|
||||
command = self.command_input.toPlainText().strip()
|
||||
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
|
||||
|
||||
# Get download path from parent
|
||||
path = self._parent.last_path
|
||||
|
||||
self.log_output.clear()
|
||||
self.log_output.append("🚀 Executing custom yt-dlp command")
|
||||
self.log_output.append(f"📍 URL: {url}")
|
||||
self.log_output.append(f"⚙️ Arguments: {command}")
|
||||
self.log_output.append(_("custom_command.executing"))
|
||||
self.log_output.append(_("custom_command.url_label", url=url))
|
||||
self.log_output.append(_("custom_command.args_label", command=command))
|
||||
if path:
|
||||
self.log_output.append(f"📁 Download path: {path}")
|
||||
self.log_output.append("=" * 50)
|
||||
self.log_output.append(_("custom_command.download_path_label", path=path))
|
||||
self.log_output.append(_("custom_command.separator"))
|
||||
self.run_btn.setEnabled(False)
|
||||
self.run_btn.setText(_("command.running"))
|
||||
|
||||
@@ -961,6 +966,12 @@ class CustomOptionsDialog(QDialog):
|
||||
logger.info(f"Saving auto-update settings: enabled={enabled}, frequency={frequency}")
|
||||
result = update_auto_update_settings(enabled, frequency)
|
||||
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:
|
||||
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.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
from src.core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed
|
||||
from src.utils.ytsage_constants import ICON_PATH
|
||||
from ...core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed
|
||||
from ...utils.ytsage_constants import ICON_PATH
|
||||
from ...utils.ytsage_localization import _
|
||||
|
||||
|
||||
class FFmpegInstallThread(QThread):
|
||||
@@ -30,7 +31,7 @@ class FFmpegInstallThread(QThread):
|
||||
class FFmpegCheckDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("FFmpeg Installation")
|
||||
self.setWindowTitle(_("ffmpeg.installation_title"))
|
||||
self.setMinimumWidth(500)
|
||||
self.setMinimumHeight(280)
|
||||
self.resize(500, 300)
|
||||
@@ -50,13 +51,13 @@ class FFmpegCheckDialog(QDialog):
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# 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.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(header_text)
|
||||
|
||||
# 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.setStyleSheet("font-size: 13px; color: #cccccc; padding: 10px 0; line-height: 1.4;")
|
||||
self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
@@ -93,17 +94,17 @@ class FFmpegCheckDialog(QDialog):
|
||||
button_layout.setSpacing(12)
|
||||
|
||||
# Install button
|
||||
self.install_btn = QPushButton("Install FFmpeg")
|
||||
self.install_btn = QPushButton(_("ffmpeg.install_button"))
|
||||
self.install_btn.clicked.connect(self.start_installation)
|
||||
button_layout.addWidget(self.install_btn)
|
||||
|
||||
# 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"))
|
||||
button_layout.addWidget(self.manual_btn)
|
||||
|
||||
# Close button
|
||||
self.close_btn = QPushButton("Close")
|
||||
self.close_btn = QPushButton(_("buttons.close"))
|
||||
self.close_btn.clicked.connect(self.close)
|
||||
button_layout.addWidget(self.close_btn)
|
||||
|
||||
@@ -153,15 +154,15 @@ class FFmpegCheckDialog(QDialog):
|
||||
|
||||
# Check if FFmpeg is already installed
|
||||
if check_ffmpeg_installed():
|
||||
self.message_label.setText("FFmpeg is already installed!")
|
||||
self.progress_label.setText("Installation complete. You can close this dialog and continue using YTSage.")
|
||||
self.message_label.setText(_("ffmpeg.already_installed"))
|
||||
self.progress_label.setText(_("ffmpeg.installation_complete"))
|
||||
self.progress_label.show()
|
||||
self.install_btn.hide()
|
||||
self.manual_btn.hide()
|
||||
self.close_btn.setEnabled(True)
|
||||
return
|
||||
|
||||
self.message_label.setText("Installing FFmpeg... Please wait")
|
||||
self.message_label.setText(_("ffmpeg.installing"))
|
||||
self.progress_messages = [] # Clear previous messages
|
||||
self.progress_label.show()
|
||||
|
||||
@@ -181,13 +182,13 @@ class FFmpegCheckDialog(QDialog):
|
||||
|
||||
def installation_finished(self, success) -> None:
|
||||
if success:
|
||||
self.message_label.setText("FFmpeg has been installed successfully!")
|
||||
self.progress_label.setText("Installation complete. You can now close this dialog and continue using YTSage.")
|
||||
self.message_label.setText(_("ffmpeg.install_success"))
|
||||
self.progress_label.setText(_("ffmpeg.installation_complete_close"))
|
||||
self.install_btn.hide()
|
||||
self.manual_btn.hide()
|
||||
else:
|
||||
self.message_label.setText("FFmpeg installation encountered an issue.")
|
||||
self.progress_label.setText("Please try using the manual installation guide instead.")
|
||||
self.message_label.setText(_("ffmpeg.installation_failed"))
|
||||
self.progress_label.setText(_("ffmpeg.try_manual"))
|
||||
self.install_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,
|
||||
)
|
||||
|
||||
from src.utils.ytsage_localization import _
|
||||
from ...utils.ytsage_localization import _
|
||||
|
||||
|
||||
class SubtitleSelectionDialog(QDialog):
|
||||
@@ -208,6 +208,27 @@ class PlaylistSelectionDialog(QDialog):
|
||||
# Main layout
|
||||
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)
|
||||
button_layout = QHBoxLayout()
|
||||
select_all_btn = QPushButton(_("buttons.select_all"))
|
||||
@@ -339,6 +360,13 @@ class PlaylistSelectionDialog(QDialog):
|
||||
pass # Ignore invalid numbers
|
||||
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:
|
||||
"""Populates the scroll area with checkboxes for each video."""
|
||||
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
|
||||
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.setProperty("video_index", video_index) # Store index
|
||||
checkbox.setProperty("full_title", title) # Store full title for filtering
|
||||
checkbox.setStyleSheet(
|
||||
"""
|
||||
QCheckBox {
|
||||
@@ -9,7 +9,8 @@ from datetime import datetime
|
||||
|
||||
import requests
|
||||
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 (
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
@@ -27,9 +28,10 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_config_manager import ConfigManager
|
||||
from ...utils.ytsage_logger import logger
|
||||
from ...utils.ytsage_localization import _
|
||||
from ...utils.ytsage_config_manager import ConfigManager
|
||||
from ...utils.ytsage_constants import APP_LOG_DIR
|
||||
|
||||
|
||||
class DownloadSettingsDialog(QDialog):
|
||||
@@ -228,7 +230,7 @@ class DownloadSettingsDialog(QDialog):
|
||||
# Help text
|
||||
help_label = QLabel(_("settings.force_format_help"))
|
||||
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_group_box.setLayout(output_format_layout)
|
||||
@@ -274,12 +276,41 @@ class DownloadSettingsDialog(QDialog):
|
||||
# Help text for audio format
|
||||
audio_help_label = QLabel(_("settings.force_audio_format_help"))
|
||||
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_group_box.setLayout(audio_format_layout)
|
||||
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)
|
||||
button_box = QDialogButtonBox()
|
||||
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"}
|
||||
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:
|
||||
"""Create a styled QMessageBox that matches the app theme."""
|
||||
msg_box = QMessageBox(self)
|
||||
@@ -382,6 +417,11 @@ class DownloadSettingsDialog(QDialog):
|
||||
ConfigManager.set("force_audio_format", force_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(
|
||||
self,
|
||||
_("settings.settings_saved_title"),
|
||||
@@ -573,7 +613,7 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
|
||||
# Update status labels
|
||||
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"]
|
||||
if last_check > 0:
|
||||
@@ -14,15 +14,16 @@ from packaging import version
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout
|
||||
|
||||
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config
|
||||
from src.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 src.utils.ytsage_localization import LocalizationManager
|
||||
from ...core.ytsage_utils import get_ytdlp_version
|
||||
from ...core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ...utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
|
||||
from ...utils.ytsage_config_manager import ConfigManager
|
||||
from ...utils.ytsage_localization import LocalizationManager
|
||||
|
||||
# Shorthand for localization
|
||||
_ = LocalizationManager.get_text
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_logger import logger
|
||||
from ...utils.ytsage_localization import _
|
||||
from ...utils.ytsage_logger import logger
|
||||
|
||||
|
||||
class VersionCheckThread(QThread):
|
||||
@@ -407,9 +408,7 @@ class AutoUpdateThread(QThread):
|
||||
if success:
|
||||
logger.info("AutoUpdateThread: Auto-update completed successfully!")
|
||||
# Update the last check timestamp
|
||||
config = load_config()
|
||||
config["last_update_check"] = time.time()
|
||||
save_config(config)
|
||||
ConfigManager.set("last_update_check", time.time())
|
||||
self.update_finished.emit(
|
||||
True,
|
||||
f"Successfully updated yt-dlp from {current_version} to {latest_version}",
|
||||
@@ -420,9 +419,7 @@ class AutoUpdateThread(QThread):
|
||||
else:
|
||||
logger.info("AutoUpdateThread: yt-dlp is already up to date")
|
||||
# Still update the timestamp even if no update was needed
|
||||
config = load_config()
|
||||
config["last_update_check"] = time.time()
|
||||
save_config(config)
|
||||
ConfigManager.set("last_update_check", time.time())
|
||||
self.update_finished.emit(
|
||||
True,
|
||||
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
|
||||
|
||||
import requests
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtCore import Qt, Signal, QThread, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QGroupBox,
|
||||
@@ -23,25 +23,25 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.core.ytsage_utils import (
|
||||
from ...core.ytsage_utils import (
|
||||
get_auto_update_settings,
|
||||
get_ffmpeg_version_direct,
|
||||
update_auto_update_settings,
|
||||
)
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.core.ytsage_deno import check_deno_update, upgrade_deno
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import YTDLPUpdateDialog
|
||||
from src.utils.ytsage_config_manager import ConfigManager
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
from ...core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ...core.ytsage_deno import check_deno_update, upgrade_deno
|
||||
from .ytsage_dialogs_update import YTDLPUpdateDialog
|
||||
from ...utils.ytsage_config_manager import ConfigManager
|
||||
from ...utils.ytsage_localization import _
|
||||
from ...utils.ytsage_logger import logger
|
||||
from ...utils.ytsage_constants import (
|
||||
FFMPEG_7Z_VERSION_URL,
|
||||
OS_NAME,
|
||||
SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -157,6 +157,47 @@ def check_ffmpeg_version() -> Tuple[bool, str, str]:
|
||||
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):
|
||||
"""Widget for the Updater tab in Custom Options dialog."""
|
||||
|
||||
@@ -401,6 +442,40 @@ class UpdaterTabWidget(QWidget):
|
||||
|
||||
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 ===
|
||||
ytdlp_channel_group = QGroupBox(_("settings.ytdlp_channel"))
|
||||
ytdlp_channel_layout = QVBoxLayout()
|
||||
@@ -466,6 +541,32 @@ class UpdaterTabWidget(QWidget):
|
||||
|
||||
# Enable/Disable auto-update checkbox
|
||||
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)
|
||||
|
||||
# Frequency options
|
||||
@@ -551,6 +652,10 @@ class UpdaterTabWidget(QWidget):
|
||||
# Set checkbox
|
||||
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
|
||||
current_frequency = auto_settings["frequency"]
|
||||
if current_frequency == "startup":
|
||||
@@ -589,6 +694,10 @@ class UpdaterTabWidget(QWidget):
|
||||
|
||||
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:
|
||||
"""Handle channel selection change."""
|
||||
if not checked:
|
||||
@@ -637,6 +746,11 @@ class UpdaterTabWidget(QWidget):
|
||||
response.raise_for_status()
|
||||
latest_tag = response.json()["info"]["version"]
|
||||
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}"
|
||||
logger.info(f"Latest stable version tag: {latest_tag}")
|
||||
else:
|
||||
@@ -754,22 +868,20 @@ class UpdaterTabWidget(QWidget):
|
||||
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
|
||||
)
|
||||
|
||||
# Run check in background thread
|
||||
def check_thread():
|
||||
try:
|
||||
update_available, current_version, latest_version = check_ffmpeg_version()
|
||||
self.ffmpeg_check_thread = FFmpegCheckThread()
|
||||
self.ffmpeg_check_thread.finished.connect(self._on_ffmpeg_check_finished)
|
||||
self.ffmpeg_check_thread.error.connect(self._on_ffmpeg_check_error)
|
||||
self.ffmpeg_check_thread.start()
|
||||
|
||||
# Update UI in main thread
|
||||
self.check_button.setEnabled(True)
|
||||
self._update_check_results(update_available, current_version, latest_version)
|
||||
@Slot(bool, str, str)
|
||||
def _on_ffmpeg_check_finished(self, update_available, current_version, latest_version):
|
||||
self.check_button.setEnabled(True)
|
||||
self._update_check_results(update_available, current_version, latest_version)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error checking FFmpeg version: {e}")
|
||||
self.check_button.setEnabled(True)
|
||||
self._show_check_error(str(e))
|
||||
|
||||
thread = threading.Thread(target=check_thread, daemon=True)
|
||||
thread.start()
|
||||
@Slot(str)
|
||||
def _on_ffmpeg_check_error(self, error):
|
||||
self.check_button.setEnabled(True)
|
||||
self._show_check_error(error)
|
||||
|
||||
def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None:
|
||||
"""Handle completion of version check."""
|
||||
@@ -819,22 +931,20 @@ class UpdaterTabWidget(QWidget):
|
||||
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
|
||||
)
|
||||
|
||||
# Run check in background thread
|
||||
def check_thread():
|
||||
try:
|
||||
update_available, current_version, latest_version = check_deno_update()
|
||||
self.deno_check_thread = DenoCheckThread()
|
||||
self.deno_check_thread.finished.connect(self._on_deno_check_finished)
|
||||
self.deno_check_thread.error.connect(self._on_deno_check_error)
|
||||
self.deno_check_thread.start()
|
||||
|
||||
# Update UI in main thread
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self._update_deno_check_results(update_available, current_version, latest_version)
|
||||
@Slot(bool, str, str)
|
||||
def _on_deno_check_finished(self, update_available, current_version, latest_version):
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self._update_deno_check_results(update_available, current_version, latest_version)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error checking Deno version: {e}")
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self._show_deno_check_error(str(e))
|
||||
|
||||
thread = threading.Thread(target=check_thread, daemon=True)
|
||||
thread.start()
|
||||
@Slot(str)
|
||||
def _on_deno_check_error(self, error):
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self._show_deno_check_error(error)
|
||||
|
||||
def _update_deno_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None:
|
||||
"""Handle completion of Deno version check."""
|
||||
@@ -882,24 +992,41 @@ class UpdaterTabWidget(QWidget):
|
||||
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
|
||||
)
|
||||
|
||||
# Run update in background thread
|
||||
def update_thread():
|
||||
try:
|
||||
success, output = upgrade_deno()
|
||||
# Use QThread for updates with progress reporting
|
||||
self.deno_update_thread = DenoUpdateThread()
|
||||
self.deno_update_thread.progress.connect(self._on_deno_update_progress)
|
||||
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
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self.deno_update_button.setEnabled(True)
|
||||
self._handle_deno_update_result(success, output)
|
||||
@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()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error updating Deno: {e}")
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self.deno_update_button.setEnabled(True)
|
||||
self._handle_deno_update_result(False, str(e))
|
||||
if not display_msg:
|
||||
return
|
||||
|
||||
thread = threading.Thread(target=update_thread, daemon=True)
|
||||
thread.start()
|
||||
# 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_update_button.setEnabled(True)
|
||||
self._handle_deno_update_result(success, output)
|
||||
|
||||
@Slot(str)
|
||||
def _on_deno_update_error(self, error: str) -> None:
|
||||
self.deno_check_button.setEnabled(True)
|
||||
self.deno_update_button.setEnabled(True)
|
||||
self._handle_deno_update_result(False, error)
|
||||
|
||||
def _handle_deno_update_result(self, success: bool, output: str) -> None:
|
||||
"""Handle Deno update completion."""
|
||||
@@ -4,10 +4,10 @@ from PySide6.QtCore import QObject, Qt, Signal
|
||||
from PySide6.QtGui import QColor, QFontMetrics
|
||||
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:
|
||||
from src.gui.ytsage_gui_main import YTSageApp
|
||||
from .ytsage_gui_main import YTSageApp
|
||||
|
||||
|
||||
class FormatSignals(QObject):
|
||||
@@ -158,6 +158,8 @@ class FormatTableMixin:
|
||||
# Store format checkboxes and formats
|
||||
self.format_checkboxes = []
|
||||
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
|
||||
self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
@@ -173,29 +175,42 @@ class FormatTableMixin:
|
||||
def filter_formats(self) -> None:
|
||||
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
|
||||
|
||||
# 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
|
||||
self.format_table.setRowCount(0)
|
||||
self.format_checkboxes.clear()
|
||||
self._row_format_type.clear()
|
||||
|
||||
# Determine which formats to show
|
||||
filtered_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
|
||||
for f in self.all_formats
|
||||
if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower())
|
||||
and f.get("acodec") != "none"
|
||||
and f.get("filesize") is not None
|
||||
]
|
||||
)
|
||||
# Separate and filter formats
|
||||
video_formats = [f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None]
|
||||
audio_formats = [
|
||||
f
|
||||
for f in self.all_formats
|
||||
if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower())
|
||||
and f.get("acodec") != "none"
|
||||
and f.get("filesize") is not None
|
||||
]
|
||||
|
||||
# Sort formats by quality
|
||||
def get_quality(f):
|
||||
@@ -211,17 +226,30 @@ class FormatTableMixin:
|
||||
else:
|
||||
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
|
||||
self.format_signals.format_update.emit(filtered_formats)
|
||||
# Combine: video first, then audio (maintains logical grouping)
|
||||
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.format_table.setRowCount(0)
|
||||
self.format_checkboxes.clear()
|
||||
|
||||
is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue]
|
||||
|
||||
# Configure columns based on mode
|
||||
@@ -252,119 +280,103 @@ class FormatTableMixin:
|
||||
_("formats.hdr"),
|
||||
]
|
||||
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)
|
||||
|
||||
|
||||
for f in formats:
|
||||
for f, format_type in formats_with_types:
|
||||
row = self.format_table.rowCount()
|
||||
self.format_table.insertRow(row)
|
||||
self._row_format_type.append(format_type)
|
||||
|
||||
# Column 0: Select Checkbox (Always shown)
|
||||
# Create checkbox widget
|
||||
checkbox = QCheckBox()
|
||||
checkbox.format_id = str(f.get("format_id", "")) # type: ignore[reportAttributeAccessIssue]
|
||||
checkbox.is_audio_only = bool((f.get("vcodec") or "none").lower() == "none") # type: ignore[attr-defined]
|
||||
checkbox.has_audio = bool(f.get("acodec") and f.get("acodec") != "none") # type: ignore[attr-defined]
|
||||
checkbox.setStyleSheet("QCheckBox { margin-left: 8px; }")
|
||||
checkbox.format_id = f["format_id"]
|
||||
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))
|
||||
self.format_checkboxes.append(checkbox)
|
||||
checkbox_widget = QWidget()
|
||||
checkbox_widget.setStyleSheet("background-color: transparent;")
|
||||
checkbox_layout = QHBoxLayout(checkbox_widget)
|
||||
|
||||
# Create a container widget for the checkbox
|
||||
checkbox_container = QWidget()
|
||||
checkbox_layout = QHBoxLayout(checkbox_container)
|
||||
checkbox_layout.addWidget(checkbox)
|
||||
checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
checkbox_layout.setContentsMargins(0, 0, 0, 0)
|
||||
checkbox_layout.setSpacing(0)
|
||||
self.format_table.setCellWidget(row, 0, checkbox_widget)
|
||||
self.format_table.setCellWidget(row, 0, checkbox_container)
|
||||
|
||||
# Column 1: Quality (Always shown)
|
||||
quality_text = self.get_quality_label(f)
|
||||
quality_item = QTableWidgetItem(quality_text)
|
||||
# Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms)
|
||||
quality_lower = quality_text.lower() # Make comparison case-insensitive
|
||||
# Quality label with color coding
|
||||
quality_label = self.get_quality_label(f)
|
||||
quality_item = QTableWidgetItem(quality_label)
|
||||
# Set color based on quality (check multiple language terms)
|
||||
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", "الأفضل", "أفضل", "最高"]):
|
||||
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
|
||||
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
|
||||
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
|
||||
self.format_table.setItem(row, 1, quality_item)
|
||||
|
||||
# --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) ---
|
||||
|
||||
# Column 2: Resolution (Always shown)
|
||||
# Resolution
|
||||
resolution = f.get("resolution", "N/A")
|
||||
if f.get("vcodec") == "none":
|
||||
resolution = _("formats.audio_only_resolution")
|
||||
self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
|
||||
|
||||
# Column 3: FPS for playlist mode, Extension for normal mode
|
||||
if is_playlist_mode:
|
||||
# Get FPS for playlist mode
|
||||
# Column 2 for playlist mode: Resolution
|
||||
self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
|
||||
|
||||
# Column 3: FPS (Frame Rate)
|
||||
fps_value = f.get("fps")
|
||||
if fps_value is not None:
|
||||
# Format FPS value appropriately
|
||||
if fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A" # Very low fps like storyboards
|
||||
if fps_value is not None and fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A"
|
||||
|
||||
fps_item = QTableWidgetItem(fps_text)
|
||||
# Color code based on FPS value
|
||||
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:
|
||||
fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps
|
||||
fps_item.setForeground(QColor("#ffaa00"))
|
||||
elif fps_value and fps_value >= 1:
|
||||
fps_item.setForeground(QColor("#ff5555")) # Red for low fps
|
||||
fps_item.setForeground(QColor("#ff5555"))
|
||||
else:
|
||||
fps_item.setForeground(QColor("#888888")) # Gray for N/A
|
||||
fps_item.setForeground(QColor("#888888"))
|
||||
self.format_table.setItem(row, 3, fps_item)
|
||||
|
||||
# Column 4: HDR for playlist mode
|
||||
# Column 4: HDR
|
||||
if f.get("vcodec") == "none":
|
||||
# Audio-only formats don't have HDR
|
||||
hdr_text = "N/A"
|
||||
hdr_item = QTableWidgetItem(hdr_text)
|
||||
hdr_item.setForeground(QColor("#888888")) # Gray for N/A
|
||||
hdr_item.setForeground(QColor("#888888"))
|
||||
else:
|
||||
hdr_value = f.get("dynamic_range")
|
||||
if hdr_value and hdr_value != "SDR":
|
||||
hdr_text = hdr_value
|
||||
hdr_item = QTableWidgetItem(hdr_text)
|
||||
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR
|
||||
hdr_item.setForeground(QColor("#00ffff"))
|
||||
else:
|
||||
hdr_text = "SDR"
|
||||
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)
|
||||
else:
|
||||
# Extension for normal mode (column 2)
|
||||
self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper()))
|
||||
|
||||
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status
|
||||
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge
|
||||
# Audio Status column
|
||||
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_item = QTableWidgetItem(audio_status)
|
||||
if needs_audio:
|
||||
audio_item.setForeground(QColor("#ffa500"))
|
||||
elif audio_status == _("formats.audio_only"):
|
||||
audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only
|
||||
else: # Has Audio (Video+Audio)
|
||||
audio_item.setForeground(QColor("#00cc00")) # Green for included audio
|
||||
# Set item for correct column based on mode
|
||||
audio_item.setForeground(QColor("#cccccc"))
|
||||
else:
|
||||
audio_item.setForeground(QColor("#00cc00"))
|
||||
audio_column_index = 5 if is_playlist_mode else 6
|
||||
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:
|
||||
# Column 3: Resolution
|
||||
self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
|
||||
@@ -384,45 +396,47 @@ class FormatTableMixin:
|
||||
|
||||
# Column 7: FPS (Frame Rate)
|
||||
fps_value = f.get("fps")
|
||||
if fps_value is not None:
|
||||
# Format FPS value appropriately
|
||||
if fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A" # Very low fps like storyboards
|
||||
if fps_value is not None and fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A"
|
||||
|
||||
fps_item = QTableWidgetItem(fps_text)
|
||||
# Color code based on FPS value
|
||||
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:
|
||||
fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps
|
||||
fps_item.setForeground(QColor("#ffaa00"))
|
||||
elif fps_value and fps_value >= 1:
|
||||
fps_item.setForeground(QColor("#ff5555")) # Red for low fps
|
||||
fps_item.setForeground(QColor("#ff5555"))
|
||||
else:
|
||||
fps_item.setForeground(QColor("#888888")) # Gray for N/A
|
||||
fps_item.setForeground(QColor("#888888"))
|
||||
self.format_table.setItem(row, 7, fps_item)
|
||||
|
||||
# Column 8: HDR (Dynamic Range)
|
||||
if f.get("vcodec") == "none":
|
||||
# Audio-only formats don't have HDR
|
||||
hdr_text = "N/A"
|
||||
hdr_item = QTableWidgetItem(hdr_text)
|
||||
hdr_item.setForeground(QColor("#888888")) # Gray for N/A
|
||||
hdr_item.setForeground(QColor("#888888"))
|
||||
else:
|
||||
hdr_value = f.get("dynamic_range")
|
||||
if hdr_value and hdr_value != "SDR":
|
||||
hdr_text = hdr_value
|
||||
hdr_item = QTableWidgetItem(hdr_text)
|
||||
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR
|
||||
hdr_item.setForeground(QColor("#00ffff"))
|
||||
else:
|
||||
hdr_text = "SDR"
|
||||
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)
|
||||
|
||||
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:
|
||||
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.all_formats = formats
|
||||
self._table_built = False # Reset flag to trigger rebuild with new formats
|
||||
self.format_signals.format_update.emit(formats)
|
||||
|
||||
def get_quality_label(self, format_info) -> str:
|
||||
@@ -6,19 +6,39 @@ from typing import TYPE_CHECKING, cast
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QPixmap
|
||||
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,
|
||||
SubtitleSelectionDialog,
|
||||
)
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_logger import logger
|
||||
from ..utils.ytsage_localization import _
|
||||
from ..utils.ytsage_logger import logger
|
||||
|
||||
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:
|
||||
@@ -290,7 +310,7 @@ class VideoInfoMixin:
|
||||
# removed extra logic for mapping to main_windows
|
||||
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()
|
||||
logger.info(f"Selected subtitles: {self.selected_subtitles}")
|
||||
# Update UI to reflect selection
|
||||
@@ -336,7 +356,7 @@ class VideoInfoMixin:
|
||||
|
||||
dialog = SponsorBlockCategoryDialog(dialog_categories, self)
|
||||
|
||||
if dialog.exec():
|
||||
if self.run_dialog_with_blur(dialog):
|
||||
self.selected_sponsorblock_categories = dialog.get_selected_categories()
|
||||
logger.info(f"SponsorBlock categories selected: {self.selected_sponsorblock_categories}")
|
||||
self._update_sponsorblock_display()
|
||||
@@ -368,14 +388,21 @@ class VideoInfoMixin:
|
||||
def download_thumbnail(self, url) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
try:
|
||||
# Store both thumbnail URL and video URL
|
||||
self.thumbnail_url = url
|
||||
self.video_url = self.url_input.text() # Get actual video URL
|
||||
# Store both thumbnail URL and video URL
|
||||
self.thumbnail_url = url
|
||||
self.video_url = self.url_input.text() # Get actual video URL
|
||||
|
||||
# Download thumbnail but don't save yet
|
||||
response = requests.get(url)
|
||||
self.thumbnail_image = Image.open(BytesIO(response.content))
|
||||
# Create and start loader thread
|
||||
# Keep reference to avoid garbage collection
|
||||
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
|
||||
image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS)
|
||||
@@ -383,9 +410,18 @@ class VideoInfoMixin:
|
||||
image.save(img_byte_arr, format="PNG")
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(img_byte_arr.getvalue())
|
||||
|
||||
# Fade in the thumbnail
|
||||
self.thumbnail_label.setVisible(False)
|
||||
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:
|
||||
logger.exception(f"Error loading thumbnail: {e}")
|
||||
logger.exception(f"Error processing thumbnail image: {e}")
|
||||
|
||||
def download_thumbnail_file(self, video_url, path) -> bool:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "صوت فقط"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "إعادة تعيين",
|
||||
"download": "تنزيل",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"resume": "استئناف",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "اختر الترجمات",
|
||||
"filter_languages_placeholder": "تصفية اللغات (مثال: ar، en)...",
|
||||
"no_subtitles_available": "لا توجد ترجمات متاحة",
|
||||
"matching": "مطابقة"
|
||||
"matching": "مطابقة",
|
||||
"ytdlp_log_title": "سجل yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "تسجيل الدخول بالكوكيز",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "تم تطبيق كوكيز المتصفح",
|
||||
"browser_applied_message": "سيتم استخراج كوكيز المتصفح من: {browser}",
|
||||
"cleared_title": "تم مسح الكوكيز",
|
||||
"cleared_message": "تم مسح إعدادات الكوكيز"
|
||||
"cleared_message": "تم مسح إعدادات الكوكيز",
|
||||
"active_browser": "✓ نشط: كوكيز المتصفح ({browser})",
|
||||
"active_file": "✓ نشط: ملف كوكيز ({file})",
|
||||
"none_active": "○ لا توجد كوكيز نشطة"
|
||||
},
|
||||
"custom_command": {
|
||||
"help_text": "أدخل أمر yt-dlp مخصص أدناه. سيتم إضافة الرابط الحالي تلقائياً.<br><br>للحصول على قائمة كاملة بالخيارات وأمثلة الاستخدام <a href=\"{docs_url}\">انقر هنا لمشاهدة الوثائق الرسمية لـ yt-dlp</a>.<br><br>ملاحظة: يتم التعامل مع مسار التنزيل ونموذج اسم الملف تلقائياً.",
|
||||
@@ -137,7 +142,14 @@
|
||||
"full_command": "🔧 الأمر الكامل: {command}",
|
||||
"command_success": "✅ تم تنفيذ الأمر المخصص بنجاح!",
|
||||
"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": {
|
||||
"help_text": "قم بتكوين إعدادات البروكسي للتنزيلات. اتركه فارغاً للاتصال المباشر.",
|
||||
@@ -159,7 +171,15 @@
|
||||
"invalid_main_url": "تنسيق رابط البروكسي الرئيسي غير صالح",
|
||||
"invalid_geo_url": "تنسيق رابط بروكسي الموقع غير صالح",
|
||||
"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": {
|
||||
"preparing": "جاري التحضير للتنزيل...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "معلومات النظام",
|
||||
"loading": "🔄 جاري تحميل معلومات النظام...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 السجلات",
|
||||
"logs_tooltip": "فتح مجلد سجلات التطبيق",
|
||||
"refreshing": "🔄 جاري التحديث...",
|
||||
"refresh_failed": "فشل التحديث",
|
||||
"refresh_failed_message": "تعذر تحديث معلومات الإصدار.",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ تم التبديل بنجاح إلى قناة {channel}!",
|
||||
"ytdlp_channel_switch_failed": "❌ فشل تبديل القناة: {error}",
|
||||
"ytdlp_current_channel": "القناة الحالية: {channel}",
|
||||
"app_updates_title": "تحديثات YTSage",
|
||||
"check_beta_updates": "تلقي تحديثات تجريبية (Beta)",
|
||||
"auto_update_title": "إعدادات التحديثات التلقائية",
|
||||
"auto_update_header": "🔄 إعدادات التحديثات التلقائية",
|
||||
"auto_update_description": "قم بتكوين التحديثات التلقائية لـ yt-dlp لضمان الحصول على أحدث الميزات وإصلاحات الأخطاء.",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV (غير مضغوط)",
|
||||
"audio_format_opus": "Opus (فعال)",
|
||||
"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": {
|
||||
"url_placeholder": "أدخل رابط فيديو يوتيوب أو قائمة تشغيل",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "سيتم استخراج كوكيز المتصفح من: {browser}",
|
||||
"error_no_format_info": "خطأ: لا توجد معلومات تنسيق متاحة.",
|
||||
"error_extract_info": "خطأ: تعذر استخراج معلومات الفيديو الأساسية. تحقق من الرابط.",
|
||||
"analyzing_preparing": "التحليل (0%)... جاري التحضير للطلب",
|
||||
"analyzing_extracting_basic": "التحليل (15%)... جاري استخراج المعلومات الأساسية",
|
||||
"analyzing_extracting_detailed": "التحليل (30%)... جاري استخراج المعلومات التفصيلية",
|
||||
"analyzing_processing_video": "التحليل (45%)... جاري معالجة بيانات الفيديو",
|
||||
"analyzing_processing_formats": "التحليل (60%)... جاري معالجة التنسيقات",
|
||||
"analyzing_loading_thumbnail": "التحليل (75%)... جاري تحميل الصورة المصغرة",
|
||||
"analyzing_processing_subtitles": "التحليل (85%)... جاري معالجة الترجمات",
|
||||
"analyzing_updating_table": "التحليل (95%)... جاري تحديث جدول التنسيقات",
|
||||
"analyzing_preparing": "جاري التحضير للطلب...",
|
||||
"analyzing_extracting_basic": "جاري استخراج المعلومات الأساسية...",
|
||||
"analyzing_extracting_detailed": "جاري استخراج المعلومات التفصيلية...",
|
||||
"analyzing_processing_video": "جاري معالجة بيانات الفيديو...",
|
||||
"analyzing_processing_formats": "جاري معالجة التنسيقات...",
|
||||
"analyzing_loading_thumbnail": "جاري تحميل الصورة المصغرة...",
|
||||
"analyzing_processing_subtitles": "جاري معالجة الترجمات...",
|
||||
"analyzing_updating_table": "جاري تحديث جدول التنسيقات...",
|
||||
"analysis_complete": "اكتمل التحليل!",
|
||||
"analyzing_extracting_ytdlp": "التحليل (30%)... جاري استخراج المعلومات",
|
||||
"analyzing_processing_data": "التحليل (60%)... جاري معالجة البيانات",
|
||||
"analyzing_processing_formats_ytdlp": "التحليل (75%)... جاري معالجة التنسيقات",
|
||||
"analyzing_loading_thumbnail_ytdlp": "التحليل (85%)... جاري تحميل الصورة المصغرة",
|
||||
"analyzing_processing_subtitles_ytdlp": "التحليل (90%)... جاري معالجة الترجمات",
|
||||
"analyzing_extracting_ytdlp": "جاري استخراج المعلومات...",
|
||||
"analyzing_fetching_first_video": "جاري جلب التنسيقات للفيديو الأول...",
|
||||
"analyzing_processing_data": "جاري معالجة البيانات...",
|
||||
"analyzing_processing_formats_ytdlp": "جاري معالجة التنسيقات...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "جاري تحميل الصورة المصغرة...",
|
||||
"analyzing_processing_subtitles_ytdlp": "جاري معالجة الترجمات...",
|
||||
"select_subtitles": "اختر الترجمات...",
|
||||
"sponsorblock_categories": "فئات SponsorBlock...",
|
||||
"invalid_url_or_enter": "عنوان URL غير صالح أو الرجاء إدخال عنوان URL.",
|
||||
"zero_selected": "تم اختيار 0",
|
||||
"analyze_first_tooltip": "يرجى تحليل الفيديو أولاً",
|
||||
"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": {
|
||||
"sponsor": "الراعي",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "خطأ: فشل yt-dlp: {error}",
|
||||
"parse_failed": "خطأ: فشل تحليل مخرجات yt-dlp: {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": {
|
||||
"title": "تحديث متاح",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "الفحص التالي: {time}",
|
||||
"next_check_error": "الفحص التالي: خطأ في الحساب",
|
||||
"checking": "🔄 جاري الفحص...",
|
||||
"check_now": "🔍 التحقق من التحديثات الآن"
|
||||
"check_now": "🔍 التحقق من التحديثات الآن",
|
||||
"current_version": "إصدار yt-dlp الحالي: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "لا يمكن أن يكون الرابط فارغًا",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "هل أنت متأكد؟ لا يمكن التراجع عن هذا.",
|
||||
"no_history": "لا يوجد سجل بعد",
|
||||
"no_history_description": "ستظهر تنزيلاتك هنا",
|
||||
"loading": "جارٍ تحميل السجل...",
|
||||
"search_placeholder": "بحث...",
|
||||
"open_location": "فتح الموقع",
|
||||
"redownload": "إعادة التنزيل",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "تنزيل واحد",
|
||||
"redownload_confirm_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": {
|
||||
"title": "مدقق إصدار FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Nur Audio"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Zurücksetzen",
|
||||
"download": "Herunterladen",
|
||||
"pause": "Pausieren",
|
||||
"resume": "Fortsetzen",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Untertitel auswählen",
|
||||
"filter_languages_placeholder": "Sprachen filtern (z.B. en, de)...",
|
||||
"no_subtitles_available": "Keine Untertitel verfügbar",
|
||||
"matching": "passend"
|
||||
"matching": "passend",
|
||||
"ytdlp_log_title": "yt-dlp-Protokoll"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Mit Cookies anmelden",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Browser-Cookies angewendet",
|
||||
"browser_applied_message": "Browser-Cookies werden extrahiert von: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Benutzerdefinierter Befehl erfolgreich ausgeführt!",
|
||||
"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": {
|
||||
"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_geo_url": "Ungültiges Geo-Proxy-URL-Format",
|
||||
"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": {
|
||||
"preparing": "Download wird vorbereitet...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Systeminformationen",
|
||||
"loading": "🔄 Systeminformationen werden geladen...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Logs",
|
||||
"logs_tooltip": "Anwendungs-Protokollordner öffnen",
|
||||
"refreshing": "🔄 Wird aktualisiert...",
|
||||
"refresh_failed": "Aktualisierung fehlgeschlagen",
|
||||
"refresh_failed_message": "Versionsinformationen konnten nicht aktualisiert werden.",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ Erfolgreich zu {channel}-Kanal gewechselt!",
|
||||
"ytdlp_channel_switch_failed": "❌ Kanalwechsel fehlgeschlagen: {error}",
|
||||
"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_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.",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV (Unkomprimiert)",
|
||||
"audio_format_opus": "Opus (Effizient)",
|
||||
"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": {
|
||||
"url_placeholder": "YouTube-Video- oder Playlist-URL eingeben",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "Browser-Cookies werden aus folgendem Browser extrahiert: {browser}",
|
||||
"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.",
|
||||
"analyzing_preparing": "Analysiere (0%)... Anfrage wird vorbereitet",
|
||||
"analyzing_extracting_basic": "Analysiere (15%)... Grundlegende Informationen werden extrahiert",
|
||||
"analyzing_extracting_detailed": "Analysiere (30%)... Detaillierte Informationen werden extrahiert",
|
||||
"analyzing_processing_video": "Analysiere (45%)... Videodaten werden verarbeitet",
|
||||
"analyzing_processing_formats": "Analysiere (60%)... Formate werden verarbeitet",
|
||||
"analyzing_loading_thumbnail": "Analysiere (75%)... Thumbnail wird geladen",
|
||||
"analyzing_processing_subtitles": "Analysiere (85%)... Untertitel werden verarbeitet",
|
||||
"analyzing_updating_table": "Analysiere (95%)... Formattabelle wird aktualisiert",
|
||||
"analyzing_preparing": "Anfrage wird vorbereitet...",
|
||||
"analyzing_extracting_basic": "Grundlegende Informationen werden extrahiert...",
|
||||
"analyzing_extracting_detailed": "Detaillierte Informationen werden extrahiert...",
|
||||
"analyzing_processing_video": "Videodaten werden verarbeitet...",
|
||||
"analyzing_processing_formats": "Formate werden verarbeitet...",
|
||||
"analyzing_loading_thumbnail": "Thumbnail wird geladen...",
|
||||
"analyzing_processing_subtitles": "Untertitel werden verarbeitet...",
|
||||
"analyzing_updating_table": "Formattabelle wird aktualisiert...",
|
||||
"analysis_complete": "Analyse abgeschlossen!",
|
||||
"analyzing_extracting_ytdlp": "Analysiere (30%)... Informationen werden extrahiert",
|
||||
"analyzing_processing_data": "Analysiere (60%)... Daten werden verarbeitet",
|
||||
"analyzing_processing_formats_ytdlp": "Analysiere (75%)... Formate werden verarbeitet",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analysiere (85%)... Thumbnail wird geladen",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analysiere (90%)... Untertitel werden verarbeitet",
|
||||
"analyzing_extracting_ytdlp": "Informationen werden extrahiert...",
|
||||
"analyzing_fetching_first_video": "Formate für das erste Video werden abgerufen...",
|
||||
"analyzing_processing_data": "Daten werden verarbeitet...",
|
||||
"analyzing_processing_formats_ytdlp": "Formate werden verarbeitet...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Thumbnail wird geladen...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Untertitel werden verarbeitet...",
|
||||
"select_subtitles": "Untertitel auswählen...",
|
||||
"sponsorblock_categories": "SponsorBlock-Kategorien...",
|
||||
"invalid_url_or_enter": "Ungültige URL oder bitte geben Sie eine URL ein.",
|
||||
"zero_selected": "0 ausgewählt",
|
||||
"analyze_first_tooltip": "Bitte analysieren Sie zuerst das Video",
|
||||
"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": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "Fehler: yt-dlp fehlgeschlagen: {error}",
|
||||
"parse_failed": "Fehler: Fehler beim Parsen der yt-dlp-Ausgabe: {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": {
|
||||
"title": "Update verfügbar",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Nächste Prüfung: {time}",
|
||||
"next_check_error": "Nächste Prüfung: Fehler bei Berechnung",
|
||||
"checking": "🔄 Prüfen...",
|
||||
"check_now": "🔍 Jetzt nach Updates suchen"
|
||||
"check_now": "🔍 Jetzt nach Updates suchen",
|
||||
"current_version": "Aktuelle yt-dlp-Version: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"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.",
|
||||
"no_history": "Noch kein Verlauf",
|
||||
"no_history_description": "Ihre Downloads werden hier angezeigt",
|
||||
"loading": "Verlauf wird geladen...",
|
||||
"search_placeholder": "Suchen...",
|
||||
"open_location": "Speicherort Öffnen",
|
||||
"redownload": "Erneut Laden",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 Download",
|
||||
"redownload_confirm_title": "Erneut Laden?",
|
||||
"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": {
|
||||
"title": "FFmpeg-Versionsprüfer",
|
||||
@@ -82,7 +82,8 @@
|
||||
"select_defaults": "Select Defaults",
|
||||
"select_all": "Select All",
|
||||
"deselect_all": "Deselect All",
|
||||
"open_folder": "Open folder location"
|
||||
"open_folder": "Open folder location",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"dialogs": {
|
||||
"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.",
|
||||
"select_subtitles": "Select Subtitles",
|
||||
"filter_languages_placeholder": "Filter languages (e.g., en, es)...",
|
||||
"filter_playlist_placeholder": "Filter videos...",
|
||||
"no_subtitles_available": "No subtitles available",
|
||||
"matching": "matching"
|
||||
"matching": "matching",
|
||||
"ytdlp_log_title": "yt-dlp Log"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Login with Cookies",
|
||||
@@ -124,7 +127,10 @@
|
||||
"browser_selected_title": "Browser Cookies Applied",
|
||||
"browser_applied_message": "Browser cookies will be extracted from: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Custom command completed successfully!",
|
||||
"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": {
|
||||
"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_geo_url": "Invalid geo proxy URL format",
|
||||
"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": {
|
||||
"preparing": "Preparing download...",
|
||||
@@ -225,6 +246,8 @@
|
||||
"system_info": "System Information",
|
||||
"loading": "🔄 Loading system information...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Logs",
|
||||
"logs_tooltip": "Open application logs folder",
|
||||
"refreshing": "🔄 Refreshing...",
|
||||
"refresh_failed": "Refresh Failed",
|
||||
"refresh_failed_message": "Could not refresh version information.",
|
||||
@@ -277,6 +300,8 @@
|
||||
"ytdlp_channel_switched": "✅ Successfully switched to {channel} channel!",
|
||||
"ytdlp_channel_switch_failed": "❌ Failed to switch channel: {error}",
|
||||
"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_header": "🔄 Auto-Update Settings",
|
||||
"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)",
|
||||
"audio_format_settings": "Audio Format Settings",
|
||||
"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:",
|
||||
"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)",
|
||||
@@ -326,27 +353,32 @@
|
||||
"browser_cookies_selected_message": "Browser cookies will be extracted from: {browser}",
|
||||
"error_no_format_info": "Error: No format information available.",
|
||||
"error_extract_info": "Error: Could not extract basic video information. Please check your link.",
|
||||
"analyzing_preparing": "Analyzing (0%)... Preparing request",
|
||||
"analyzing_extracting_basic": "Analyzing (15%)... Extracting basic info",
|
||||
"analyzing_extracting_detailed": "Analyzing (30%)... Extracting detailed info",
|
||||
"analyzing_processing_video": "Analyzing (45%)... Processing video data",
|
||||
"analyzing_processing_formats": "Analyzing (60%)... Processing formats",
|
||||
"analyzing_loading_thumbnail": "Analyzing (75%)... Loading thumbnail",
|
||||
"analyzing_processing_subtitles": "Analyzing (85%)... Processing subtitles",
|
||||
"analyzing_updating_table": "Analyzing (95%)... Updating format table",
|
||||
"analyzing_preparing": "Preparing request...",
|
||||
"analyzing_extracting_basic": "Extracting basic info...",
|
||||
"analyzing_extracting_detailed": "Extracting detailed info...",
|
||||
"analyzing_processing_video": "Processing video data...",
|
||||
"analyzing_processing_formats": "Processing formats...",
|
||||
"analyzing_loading_thumbnail": "Loading thumbnail...",
|
||||
"analyzing_processing_subtitles": "Processing subtitles...",
|
||||
"analyzing_updating_table": "Updating format table...",
|
||||
"analysis_complete": "Analysis complete!",
|
||||
"analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info",
|
||||
"analyzing_processing_data": "Analyzing (60%)... Processing data",
|
||||
"analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analyzing (90%)... Processing subtitles",
|
||||
"analyzing_extracting_ytdlp": "Extracting info...",
|
||||
"analyzing_fetching_first_video": "Fetching formats for first video...",
|
||||
"analyzing_processing_data": "Processing data...",
|
||||
"analyzing_processing_formats_ytdlp": "Processing formats...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Loading thumbnail...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Processing subtitles...",
|
||||
"select_subtitles": "Select Subtitles...",
|
||||
"sponsorblock_categories": "SponsorBlock Categories...",
|
||||
"invalid_url_or_enter": "Invalid URL or please enter a URL.",
|
||||
"zero_selected": "0 selected",
|
||||
"analyze_first_tooltip": "Please analyze the video first",
|
||||
"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": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +440,10 @@
|
||||
"ytdlp_failed": "Error: yt-dlp failed: {error}",
|
||||
"parse_failed": "Error: Failed to parse yt-dlp output: {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": {
|
||||
"title": "Update Available",
|
||||
@@ -442,7 +477,8 @@
|
||||
"next_check": "Next check: {time}",
|
||||
"next_check_error": "Next check: Error calculating",
|
||||
"checking": "🔄 Checking...",
|
||||
"check_now": "🔍 Check for Updates Now"
|
||||
"check_now": "🔍 Check for Updates Now",
|
||||
"current_version": "Current yt-dlp version: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"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.",
|
||||
"no_history": "No download history yet",
|
||||
"no_history_description": "Your downloaded videos and audio will appear here",
|
||||
"loading": "Loading history...",
|
||||
"search_placeholder": "Search history...",
|
||||
"open_location": "Open File Location",
|
||||
"redownload": "Redownload",
|
||||
@@ -491,7 +528,47 @@
|
||||
"one_entry": "1 download",
|
||||
"redownload_confirm_title": "Redownload Video?",
|
||||
"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": {
|
||||
"title": "FFmpeg Version Checker",
|
||||
@@ -25,6 +25,7 @@
|
||||
"ready": "Listo"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Restablecer",
|
||||
"download": "Descargar",
|
||||
"pause": "Pausar",
|
||||
"resume": "Reanudar",
|
||||
@@ -60,7 +61,8 @@
|
||||
"select_subtitles": "Seleccionar Subtítulos",
|
||||
"filter_languages_placeholder": "Filtrar idiomas (ej., en, es)...",
|
||||
"no_subtitles_available": "No hay subtítulos disponibles",
|
||||
"matching": "que coincidan con"
|
||||
"matching": "que coincidan con",
|
||||
"ytdlp_log_title": "Registro de yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Iniciar sesión con Cookies",
|
||||
@@ -91,7 +93,10 @@
|
||||
"browser_selected_title": "Cookies del Navegador Aplicadas",
|
||||
"browser_applied_message": "Las cookies del navegador se extraerán de: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_failed": "❌ El comando falló con código de salida {code}",
|
||||
"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": {
|
||||
"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_geo_url": "Formato de URL de geo proxy inválido",
|
||||
"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": {
|
||||
"preparing": "Preparando descarga...",
|
||||
@@ -185,6 +205,8 @@
|
||||
"system_info": "Información del Sistema",
|
||||
"loading": "🔄 Cargando información del sistema...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Registros",
|
||||
"logs_tooltip": "Abrir carpeta de registros de la aplicación",
|
||||
"refreshing": "🔄 Actualizando...",
|
||||
"refresh_failed": "Actualización Fallida",
|
||||
"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_switch_failed": "❌ Error al cambiar de canal: {error}",
|
||||
"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_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.",
|
||||
@@ -290,7 +314,9 @@
|
||||
"audio_format_wav": "WAV (Sin comprimir)",
|
||||
"audio_format_opus": "Opus (Eficiente)",
|
||||
"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": {
|
||||
"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}",
|
||||
"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.",
|
||||
"analyzing_preparing": "Analizando (0%)... Preparando solicitud",
|
||||
"analyzing_extracting_basic": "Analizando (15%)... Extrayendo información básica",
|
||||
"analyzing_extracting_detailed": "Analizando (30%)... Extrayendo información detallada",
|
||||
"analyzing_processing_video": "Analizando (45%)... Procesando datos de video",
|
||||
"analyzing_processing_formats": "Analizando (60%)... Procesando formatos",
|
||||
"analyzing_loading_thumbnail": "Analizando (75%)... Cargando miniatura",
|
||||
"analyzing_processing_subtitles": "Analizando (85%)... Procesando subtítulos",
|
||||
"analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos",
|
||||
"analyzing_preparing": "Preparando solicitud...",
|
||||
"analyzing_extracting_basic": "Extrayendo información básica...",
|
||||
"analyzing_extracting_detailed": "Extrayendo información detallada...",
|
||||
"analyzing_processing_video": "Procesando datos de video...",
|
||||
"analyzing_processing_formats": "Procesando formatos...",
|
||||
"analyzing_loading_thumbnail": "Cargando miniatura...",
|
||||
"analyzing_processing_subtitles": "Procesando subtítulos...",
|
||||
"analyzing_updating_table": "Actualizando tabla de formatos...",
|
||||
"analysis_complete": "¡Análisis completo!",
|
||||
"analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo información",
|
||||
"analyzing_processing_data": "Analizando (60%)... Procesando datos",
|
||||
"analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analizando (90%)... Procesando subtítulos",
|
||||
"analyzing_extracting_ytdlp": "Extrayendo información...",
|
||||
"analyzing_fetching_first_video": "Obteniendo formatos del primer video...",
|
||||
"analyzing_processing_data": "Procesando datos...",
|
||||
"analyzing_processing_formats_ytdlp": "Procesando formatos...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Cargando miniatura...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Procesando subtítulos...",
|
||||
"select_subtitles": "Seleccionar Subtítulos...",
|
||||
"sponsorblock_categories": "Categorías SponsorBlock...",
|
||||
"invalid_url_or_enter": "URL inválida o por favor ingresa una URL.",
|
||||
"zero_selected": "0 seleccionados",
|
||||
"analyze_first_tooltip": "Por favor analiza el video primero",
|
||||
"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": {
|
||||
"sponsor": "Patrocinador",
|
||||
@@ -391,7 +422,10 @@
|
||||
"ytdlp_failed": "Error: yt-dlp falló: {error}",
|
||||
"parse_failed": "Error: Falló al analizar salida de yt-dlp: {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": {
|
||||
"title": "Actualización disponible",
|
||||
@@ -425,7 +459,8 @@
|
||||
"next_check": "Próxima verificación: {time}",
|
||||
"next_check_error": "Próxima verificación: Error al calcular",
|
||||
"checking": "🔄 Verificando...",
|
||||
"check_now": "🔍 Verificar actualizaciones ahora"
|
||||
"check_now": "🔍 Verificar actualizaciones ahora",
|
||||
"current_version": "Versión actual de yt-dlp: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"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.",
|
||||
"no_history": "Aún no hay historial",
|
||||
"no_history_description": "Tus descargas aparecerán aquí",
|
||||
"loading": "Cargando historial...",
|
||||
"search_placeholder": "Buscar...",
|
||||
"open_location": "Abrir Ubicación",
|
||||
"redownload": "Descargar de Nuevo",
|
||||
@@ -474,7 +510,47 @@
|
||||
"one_entry": "1 descarga",
|
||||
"redownload_confirm_title": "¿Descargar de Nuevo?",
|
||||
"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": {
|
||||
"title": "Comprobador de Versión FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Audio uniquement"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Réinitialiser",
|
||||
"download": "Télécharger",
|
||||
"pause": "Pause",
|
||||
"resume": "Reprendre",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Sélectionner les sous-titres",
|
||||
"filter_languages_placeholder": "Filtrer les langues (ex: en, fr)...",
|
||||
"no_subtitles_available": "Aucun sous-titre disponible",
|
||||
"matching": "correspondant"
|
||||
"matching": "correspondant",
|
||||
"ytdlp_log_title": "Journal yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Se connecter avec des cookies",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Cookies du navigateur appliqués",
|
||||
"browser_applied_message": "Les cookies du navigateur seront extraits de : {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Commande personnalisée exécutée avec succès !",
|
||||
"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": {
|
||||
"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_geo_url": "Format d'URL de proxy géographique invalide",
|
||||
"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": {
|
||||
"preparing": "Préparation du téléchargement...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Informations système",
|
||||
"loading": "🔄 Chargement des informations système...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Journaux",
|
||||
"logs_tooltip": "Ouvrir le dossier des journaux d'application",
|
||||
"refreshing": "🔄 Actualisation...",
|
||||
"refresh_failed": "Échec de l'actualisation",
|
||||
"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_switch_failed": "❌ Échec du changement de canal : {error}",
|
||||
"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_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.",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV (Non compressé)",
|
||||
"audio_format_opus": "Opus (Efficace)",
|
||||
"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": {
|
||||
"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}",
|
||||
"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.",
|
||||
"analyzing_preparing": "Analyse (0%)... Préparation de la requête",
|
||||
"analyzing_extracting_basic": "Analyse (15%)... Extraction des informations de base",
|
||||
"analyzing_extracting_detailed": "Analyse (30%)... Extraction des informations détaillées",
|
||||
"analyzing_processing_video": "Analyse (45%)... Traitement des données vidéo",
|
||||
"analyzing_processing_formats": "Analyse (60%)... Traitement des formats",
|
||||
"analyzing_loading_thumbnail": "Analyse (75%)... Chargement de la miniature",
|
||||
"analyzing_processing_subtitles": "Analyse (85%)... Traitement des sous-titres",
|
||||
"analyzing_updating_table": "Analyse (95%)... Mise à jour du tableau des formats",
|
||||
"analyzing_preparing": "Préparation de la requête...",
|
||||
"analyzing_extracting_basic": "Extraction des informations de base...",
|
||||
"analyzing_extracting_detailed": "Extraction des informations détaillées...",
|
||||
"analyzing_processing_video": "Traitement des données vidéo...",
|
||||
"analyzing_processing_formats": "Traitement des formats...",
|
||||
"analyzing_loading_thumbnail": "Chargement de la miniature...",
|
||||
"analyzing_processing_subtitles": "Traitement des sous-titres...",
|
||||
"analyzing_updating_table": "Mise à jour du tableau des formats...",
|
||||
"analysis_complete": "Analyse terminée !",
|
||||
"analyzing_extracting_ytdlp": "Analyse (30%)... Extraction d'informations",
|
||||
"analyzing_processing_data": "Analyse (60%)... Traitement des données",
|
||||
"analyzing_processing_formats_ytdlp": "Analyse (75%)... Traitement des formats",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analyse (85%)... Chargement de la miniature",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analyse (90%)... Traitement des sous-titres",
|
||||
"analyzing_extracting_ytdlp": "Extraction d'informations...",
|
||||
"analyzing_fetching_first_video": "Récupération des formats pour la première vidéo...",
|
||||
"analyzing_processing_data": "Traitement des données...",
|
||||
"analyzing_processing_formats_ytdlp": "Traitement des formats...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Chargement de la miniature...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Traitement des sous-titres...",
|
||||
"select_subtitles": "Sélectionner les sous-titres...",
|
||||
"sponsorblock_categories": "Catégories SponsorBlock...",
|
||||
"invalid_url_or_enter": "URL invalide ou veuillez entrer une URL.",
|
||||
"zero_selected": "0 sélectionné",
|
||||
"analyze_first_tooltip": "Veuillez d'abord analyser la vidéo",
|
||||
"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 d’ouvrir le dossier : {error}",
|
||||
"time_range_set": "Section définie : {section}"
|
||||
},
|
||||
"sponsorblock": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "Erreur : yt-dlp a échoué : {error}",
|
||||
"parse_failed": "Erreur : Échec de l'analyse de la sortie yt-dlp : {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 l’application.",
|
||||
"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": {
|
||||
"title": "Mise à jour disponible",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Prochaine vérification : {time}",
|
||||
"next_check_error": "Prochaine vérification : Erreur de calcul",
|
||||
"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": {
|
||||
"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é.",
|
||||
"no_history": "Pas encore d'historique",
|
||||
"no_history_description": "Vos téléchargements apparaîtront ici",
|
||||
"loading": "Chargement de l’historique...",
|
||||
"search_placeholder": "Rechercher...",
|
||||
"open_location": "Ouvrir l'Emplacement",
|
||||
"redownload": "Télécharger à Nouveau",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 téléchargement",
|
||||
"redownload_confirm_title": "Télécharger à Nouveau?",
|
||||
"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 l’entrée d’historique",
|
||||
"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 d’installation ci‑dessous :",
|
||||
"install_button": "Installer FFmpeg",
|
||||
"manual_guide": "Guide manuel",
|
||||
"installation_failed": "L’installation 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 d’utiliser le guide d’installation 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 l’application. YTSage doit configurer yt-dlp pour votre système {os_name}.<br><br>Veuillez choisir une option ci‑dessous :",
|
||||
"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 l’exé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 l’application : {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 l’exé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 à l’emplacement :\n{path}",
|
||||
"file_filter_windows": "Fichiers exécutables (*.exe)",
|
||||
"file_filter_all": "Tous les fichiers (*)"
|
||||
},
|
||||
"ffmpeg_updater": {
|
||||
"title": "Vérificateur de Version FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "केवल ऑडियो"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "रीसेट",
|
||||
"download": "डाउनलोड",
|
||||
"pause": "रोकें",
|
||||
"resume": "जारी रखें",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "उपशीर्षक चुनें",
|
||||
"filter_languages_placeholder": "भाषाएं फ़िल्टर करें (जैसे: hi, en)...",
|
||||
"no_subtitles_available": "कोई उपशीर्षक उपलब्ध नहीं",
|
||||
"matching": "मेल खाता"
|
||||
"matching": "मेल खाता",
|
||||
"ytdlp_log_title": "yt-dlp लॉग"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "कुकीज़ के साथ लॉगिन",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "ब्राउज़र कुकीज़ लागू की गईं",
|
||||
"browser_applied_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}",
|
||||
"cleared_title": "कुकीज़ साफ़ की गईं",
|
||||
"cleared_message": "कुकी सेटिंग्स साफ़ कर दी गई हैं"
|
||||
"cleared_message": "कुकी सेटिंग्स साफ़ कर दी गई हैं",
|
||||
"active_browser": "✓ सक्रिय: ब्राउज़र कुकीज़ ({browser})",
|
||||
"active_file": "✓ सक्रिय: कुकी फ़ाइल ({file})",
|
||||
"none_active": "○ कोई कुकी सक्रिय नहीं"
|
||||
},
|
||||
"custom_command": {
|
||||
"help_text": "नीचे अपना कस्टम yt-dlp कमांड दर्ज करें। वर्तमान URL स्वचालित रूप से जोड़ा जाएगा।<br><br>विकल्पों की पूरी सूची और उपयोग के उदाहरणों के लिए <a href=\"{docs_url}\">आधिकारिक yt-dlp दस्तावेज़ देखने के लिए यहाँ क्लिक करें</a>।<br><br>नोट: डाउनलोड पथ और फ़ाइलनाम टेम्प्लेट स्वचालित रूप से संभाले जाते हैं।",
|
||||
@@ -137,7 +142,14 @@
|
||||
"full_command": "🔧 पूरा कमांड: {command}",
|
||||
"command_success": "✅ कस्टम कमांड सफलतापूर्वक निष्पादित!",
|
||||
"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": {
|
||||
"help_text": "डाउनलोड के लिए प्रॉक्सी सेटिंग्स कॉन्फ़िगर करें। प्रत्यक्ष कनेक्शन के लिए खाली छोड़ें।",
|
||||
@@ -159,7 +171,15 @@
|
||||
"invalid_main_url": "अमान्य मुख्य प्रॉक्सी URL प्रारूप",
|
||||
"invalid_geo_url": "अमान्य भू-प्रॉक्सी URL प्रारूप",
|
||||
"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": {
|
||||
"preparing": "डाउनलोड तैयार हो रहा है...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "सिस्टम जानकारी",
|
||||
"loading": "🔄 सिस्टम जानकारी लोड हो रही है...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 लॉग्स",
|
||||
"logs_tooltip": "एप्लिकेशन लॉग फ़ोल्डर खोलें",
|
||||
"refreshing": "🔄 रिफ्रेश हो रहा है...",
|
||||
"refresh_failed": "रिफ्रेश असफल",
|
||||
"refresh_failed_message": "संस्करण जानकारी रिफ्रेश करने में असमर्थ।",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ सफलतापूर्वक {channel} चैनल पर स्विच किया गया!",
|
||||
"ytdlp_channel_switch_failed": "❌ चैनल स्विच करना विफल: {error}",
|
||||
"ytdlp_current_channel": "वर्तमान चैनल: {channel}",
|
||||
"app_updates_title": "YTSage अपडेट",
|
||||
"check_beta_updates": "बीटा अपडेट प्राप्त करें",
|
||||
"auto_update_title": "स्वचालित अपडेट सेटिंग्स",
|
||||
"auto_update_header": "🔄 स्वचालित अपडेट सेटिंग्स",
|
||||
"auto_update_description": "yt-dlp के लिए स्वचालित अपडेट कॉन्फ़िगर करें ताकि आपके पास हमेशा नवीनतम सुविधाएं और बग फिक्स हों।",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV (असंकुचित)",
|
||||
"audio_format_opus": "Opus (कुशल)",
|
||||
"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": {
|
||||
"url_placeholder": "YouTube वीडियो या प्लेलिस्ट URL दर्ज करें",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}",
|
||||
"error_no_format_info": "त्रुटि: कोई प्रारूप जानकारी उपलब्ध नहीं।",
|
||||
"error_extract_info": "त्रुटि: बुनियादी वीडियो जानकारी निकालने में असमर्थ। कृपया अपना लिंक जांचें।",
|
||||
"analyzing_preparing": "विश्लेषण (0%)... अनुरोध तैयार हो रहा है",
|
||||
"analyzing_extracting_basic": "विश्लेषण (15%)... बुनियादी जानकारी निकाली जा रही है",
|
||||
"analyzing_extracting_detailed": "विश्लेषण (30%)... विस्तृत जानकारी निकाली जा रही है",
|
||||
"analyzing_processing_video": "विश्लेषण (45%)... वीडियो डेटा प्रोसेस हो रहा है",
|
||||
"analyzing_processing_formats": "विश्लेषण (60%)... प्रारूप प्रोसेस हो रहे हैं",
|
||||
"analyzing_loading_thumbnail": "विश्लेषण (75%)... थंबनेल लोड हो रहा है",
|
||||
"analyzing_processing_subtitles": "विश्लेषण (85%)... उपशीर्षक प्रोसेस हो रहे हैं",
|
||||
"analyzing_updating_table": "विश्लेषण (95%)... प्रारूप तालिका अपडेट हो रही है",
|
||||
"analyzing_preparing": "अनुरोध तैयार हो रहा है...",
|
||||
"analyzing_extracting_basic": "बुनियादी जानकारी निकाली जा रही है...",
|
||||
"analyzing_extracting_detailed": "विस्तृत जानकारी निकाली जा रही है...",
|
||||
"analyzing_processing_video": "वीडियो डेटा प्रोसेस हो रहा है...",
|
||||
"analyzing_processing_formats": "प्रारूप प्रोसेस हो रहे हैं...",
|
||||
"analyzing_loading_thumbnail": "थंबनेल लोड हो रहा है...",
|
||||
"analyzing_processing_subtitles": "उपशीर्षक प्रोसेस हो रहे हैं...",
|
||||
"analyzing_updating_table": "प्रारूप तालिका अपडेट हो रही है...",
|
||||
"analysis_complete": "विश्लेषण पूर्ण!",
|
||||
"analyzing_extracting_ytdlp": "विश्लेषण (30%)... जानकारी निकाली जा रही है",
|
||||
"analyzing_processing_data": "विश्लेषण (60%)... डेटा प्रोसेस हो रहा है",
|
||||
"analyzing_processing_formats_ytdlp": "विश्लेषण (75%)... प्रारूप प्रोसेस हो रहे हैं",
|
||||
"analyzing_loading_thumbnail_ytdlp": "विश्लेषण (85%)... थंबनेल लोड हो रहा है",
|
||||
"analyzing_processing_subtitles_ytdlp": "विश्लेषण (90%)... उपशीर्षक प्रोसेस हो रहे हैं",
|
||||
"analyzing_extracting_ytdlp": "जानकारी निकाली जा रही है...",
|
||||
"analyzing_fetching_first_video": "पहले वीडियो के लिए प्रारूप प्राप्त कर रहा है...",
|
||||
"analyzing_processing_data": "डेटा प्रोसेस हो रहा है...",
|
||||
"analyzing_processing_formats_ytdlp": "प्रारूप प्रोसेस हो रहे हैं...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "थंबनेल लोड हो रहा है...",
|
||||
"analyzing_processing_subtitles_ytdlp": "उपशीर्षक प्रोसेस हो रहे हैं...",
|
||||
"select_subtitles": "उपशीर्षक चुनें...",
|
||||
"sponsorblock_categories": "SponsorBlock श्रेणियां...",
|
||||
"invalid_url_or_enter": "अमान्य URL या कृपया URL दर्ज करें।",
|
||||
"zero_selected": "0 चयनित",
|
||||
"analyze_first_tooltip": "कृपया पहले वीडियो का विश्लेषण करें",
|
||||
"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": {
|
||||
"sponsor": "प्रायोजक",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "त्रुटि: yt-dlp असफल: {error}",
|
||||
"parse_failed": "त्रुटि: yt-dlp आउटपुट पार्स करने में विफल: {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": {
|
||||
"title": "अपडेट उपलब्ध",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "अगली जांच: {time}",
|
||||
"next_check_error": "अगली जांच: गणना त्रुटि",
|
||||
"checking": "🔄 जांच रहे हैं...",
|
||||
"check_now": "🔍 अभी अपडेट की जांच करें"
|
||||
"check_now": "🔍 अभी अपडेट की जांच करें",
|
||||
"current_version": "वर्तमान yt-dlp संस्करण: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "URL खाली नहीं हो सकता",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "क्या आप निश्चित हैं? इसे पूर्ववत नहीं किया जा सकता।",
|
||||
"no_history": "अभी तक कोई इतिहास नहीं",
|
||||
"no_history_description": "आपके डाउनलोड यहां दिखाई देंगे",
|
||||
"loading": "इतिहास लोड हो रहा है...",
|
||||
"search_placeholder": "खोजें...",
|
||||
"open_location": "स्थान खोलें",
|
||||
"redownload": "फिर से डाउनलोड करें",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 डाउनलोड",
|
||||
"redownload_confirm_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": {
|
||||
"title": "FFmpeg संस्करण जांचकर्ता",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Audio saja"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Atur Ulang",
|
||||
"download": "Unduh",
|
||||
"pause": "Jeda",
|
||||
"resume": "Lanjutkan",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Pilih subtitle",
|
||||
"filter_languages_placeholder": "Filter bahasa (misalnya: id, en)...",
|
||||
"no_subtitles_available": "Tidak ada subtitle yang tersedia",
|
||||
"matching": "yang cocok"
|
||||
"matching": "yang cocok",
|
||||
"ytdlp_log_title": "Log yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Masuk dengan cookies",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Cookies Browser Diterapkan",
|
||||
"browser_applied_message": "Cookies browser akan diekstrak dari: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Perintah khusus berhasil dijalankan!",
|
||||
"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": {
|
||||
"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_geo_url": "Format URL proxy geografis tidak valid",
|
||||
"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": {
|
||||
"preparing": "Mempersiapkan unduhan...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Informasi sistem",
|
||||
"loading": "🔄 Memuat informasi sistem...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Log",
|
||||
"logs_tooltip": "Buka folder log aplikasi",
|
||||
"refreshing": "🔄 Menyegarkan...",
|
||||
"refresh_failed": "Gagal menyegarkan",
|
||||
"refresh_failed_message": "Tidak dapat menyegarkan informasi versi.",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ Berhasil beralih ke saluran {channel}!",
|
||||
"ytdlp_channel_switch_failed": "❌ Gagal beralih saluran: {error}",
|
||||
"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_header": "🔄 Pengaturan pembaruan otomatis",
|
||||
"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_opus": "Opus (Efisien)",
|
||||
"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": {
|
||||
"url_placeholder": "Masukkan URL video atau playlist YouTube",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "Cookies browser akan diekstrak dari: {browser}",
|
||||
"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.",
|
||||
"analyzing_preparing": "Menganalisis (0%)... Mempersiapkan permintaan",
|
||||
"analyzing_extracting_basic": "Menganalisis (15%)... Mengekstrak informasi dasar",
|
||||
"analyzing_extracting_detailed": "Menganalisis (30%)... Mengekstrak informasi detail",
|
||||
"analyzing_processing_video": "Menganalisis (45%)... Memproses data video",
|
||||
"analyzing_processing_formats": "Menganalisis (60%)... Memproses format",
|
||||
"analyzing_loading_thumbnail": "Menganalisis (75%)... Memuat thumbnail",
|
||||
"analyzing_processing_subtitles": "Menganalisis (85%)... Memproses subtitle",
|
||||
"analyzing_updating_table": "Menganalisis (95%)... Memperbarui tabel format",
|
||||
"analyzing_preparing": "Mempersiapkan permintaan...",
|
||||
"analyzing_extracting_basic": "Mengekstrak informasi dasar...",
|
||||
"analyzing_extracting_detailed": "Mengekstrak informasi detail...",
|
||||
"analyzing_processing_video": "Memproses data video...",
|
||||
"analyzing_processing_formats": "Memproses format...",
|
||||
"analyzing_loading_thumbnail": "Memuat thumbnail...",
|
||||
"analyzing_processing_subtitles": "Memproses subtitle...",
|
||||
"analyzing_updating_table": "Memperbarui tabel format...",
|
||||
"analysis_complete": "Analisis selesai!",
|
||||
"analyzing_extracting_ytdlp": "Menganalisis (30%)... Mengekstrak informasi",
|
||||
"analyzing_processing_data": "Menganalisis (60%)... Memproses data",
|
||||
"analyzing_processing_formats_ytdlp": "Menganalisis (75%)... Memproses format",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Menganalisis (85%)... Memuat thumbnail",
|
||||
"analyzing_processing_subtitles_ytdlp": "Menganalisis (90%)... Memproses subtitle",
|
||||
"analyzing_extracting_ytdlp": "Mengekstrak informasi...",
|
||||
"analyzing_fetching_first_video": "Mengambil format untuk video pertama...",
|
||||
"analyzing_processing_data": "Memproses data...",
|
||||
"analyzing_processing_formats_ytdlp": "Memproses format...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Memuat thumbnail...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Memproses subtitle...",
|
||||
"select_subtitles": "Pilih subtitle...",
|
||||
"sponsorblock_categories": "Kategori SponsorBlock...",
|
||||
"invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.",
|
||||
"zero_selected": "0 dipilih",
|
||||
"analyze_first_tooltip": "Silakan analisis video terlebih dahulu",
|
||||
"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": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "Kesalahan: yt-dlp gagal: {error}",
|
||||
"parse_failed": "Kesalahan: Gagal mengurai output yt-dlp: {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": {
|
||||
"title": "Pembaruan Tersedia",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Pemeriksaan berikutnya: {time}",
|
||||
"next_check_error": "Pemeriksaan berikutnya: Kesalahan perhitungan",
|
||||
"checking": "🔄 Memeriksa...",
|
||||
"check_now": "🔍 Periksa pembaruan sekarang"
|
||||
"check_now": "🔍 Periksa pembaruan sekarang",
|
||||
"current_version": "Versi yt-dlp saat ini: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "URL tidak boleh kosong",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "Apakah Anda yakin? Ini tidak dapat dibatalkan.",
|
||||
"no_history": "Belum ada riwayat",
|
||||
"no_history_description": "Unduhan Anda akan muncul di sini",
|
||||
"loading": "Memuat riwayat...",
|
||||
"search_placeholder": "Cari...",
|
||||
"open_location": "Buka Lokasi",
|
||||
"redownload": "Unduh Lagi",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 unduhan",
|
||||
"redownload_confirm_title": "Unduh Lagi?",
|
||||
"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": {
|
||||
"title": "Pemeriksa Versi FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Solo audio"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Reimposta",
|
||||
"download": "Scarica",
|
||||
"pause": "Pausa",
|
||||
"resume": "Riprendi",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Seleziona sottotitoli",
|
||||
"filter_languages_placeholder": "Filtra lingue (es: it, en)...",
|
||||
"no_subtitles_available": "Nessun sottotitolo disponibile",
|
||||
"matching": "corrispondenti"
|
||||
"matching": "corrispondenti",
|
||||
"ytdlp_log_title": "Registro yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Accedi con i cookie",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Cookie del Browser Applicati",
|
||||
"browser_applied_message": "I cookie del browser verranno estratti da: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Comando personalizzato eseguito con successo!",
|
||||
"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": {
|
||||
"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_geo_url": "Formato URL proxy geo non valido",
|
||||
"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": {
|
||||
"preparing": "Preparazione download...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Informazioni sistema",
|
||||
"loading": "🔄 Caricamento informazioni sistema...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Log",
|
||||
"logs_tooltip": "Apri cartella log applicazione",
|
||||
"refreshing": "🔄 Aggiornamento...",
|
||||
"refresh_failed": "Aggiornamento fallito",
|
||||
"refresh_failed_message": "Impossibile aggiornare le informazioni sulla versione.",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ Passaggio al canale {channel} riuscito!",
|
||||
"ytdlp_channel_switch_failed": "❌ Impossibile cambiare canale: {error}",
|
||||
"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_header": "🔄 Impostazioni aggiornamenti automatici",
|
||||
"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_opus": "Opus (Efficiente)",
|
||||
"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": {
|
||||
"url_placeholder": "Inserisci URL video YouTube o playlist",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "I cookie del browser verranno estratti da: {browser}",
|
||||
"error_no_format_info": "Errore: Nessuna informazione formato disponibile.",
|
||||
"error_extract_info": "Errore: Impossibile estrarre informazioni base del video. Controlla il tuo link.",
|
||||
"analyzing_preparing": "Analisi (0%)... Preparazione richiesta",
|
||||
"analyzing_extracting_basic": "Analisi (15%)... Estrazione informazioni base",
|
||||
"analyzing_extracting_detailed": "Analisi (30%)... Estrazione informazioni dettagliate",
|
||||
"analyzing_processing_video": "Analisi (45%)... Elaborazione dati video",
|
||||
"analyzing_processing_formats": "Analisi (60%)... Elaborazione formati",
|
||||
"analyzing_loading_thumbnail": "Analisi (75%)... Caricamento miniatura",
|
||||
"analyzing_processing_subtitles": "Analisi (85%)... Elaborazione sottotitoli",
|
||||
"analyzing_updating_table": "Analisi (95%)... Aggiornamento tabella formati",
|
||||
"analyzing_preparing": "Preparazione richiesta...",
|
||||
"analyzing_extracting_basic": "Estrazione informazioni base...",
|
||||
"analyzing_extracting_detailed": "Estrazione informazioni dettagliate...",
|
||||
"analyzing_processing_video": "Elaborazione dati video...",
|
||||
"analyzing_processing_formats": "Elaborazione formati...",
|
||||
"analyzing_loading_thumbnail": "Caricamento miniatura...",
|
||||
"analyzing_processing_subtitles": "Elaborazione sottotitoli...",
|
||||
"analyzing_updating_table": "Aggiornamento tabella formati...",
|
||||
"analysis_complete": "Analisi completata!",
|
||||
"analyzing_extracting_ytdlp": "Analisi (30%)... Estrazione informazioni",
|
||||
"analyzing_processing_data": "Analisi (60%)... Elaborazione dati",
|
||||
"analyzing_processing_formats_ytdlp": "Analisi (75%)... Elaborazione formati",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analisi (85%)... Caricamento miniatura",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analisi (90%)... Elaborazione sottotitoli",
|
||||
"analyzing_extracting_ytdlp": "Estrazione informazioni...",
|
||||
"analyzing_fetching_first_video": "Recupero dei formati per il primo video...",
|
||||
"analyzing_processing_data": "Elaborazione dati...",
|
||||
"analyzing_processing_formats_ytdlp": "Elaborazione formati...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Caricamento miniatura...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Elaborazione sottotitoli...",
|
||||
"select_subtitles": "Seleziona sottotitoli...",
|
||||
"sponsorblock_categories": "Categorie SponsorBlock...",
|
||||
"invalid_url_or_enter": "URL non valido o inserisci un URL.",
|
||||
"zero_selected": "0 selezionati",
|
||||
"analyze_first_tooltip": "Si prega di analizzare prima il video",
|
||||
"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": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "Errore: yt-dlp fallito: {error}",
|
||||
"parse_failed": "Errore: Impossibile analizzare l'output di yt-dlp: {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 l’app.",
|
||||
"download_failed_return_code": "Download non riuscito con codice di uscita {return_code}",
|
||||
"direct_command_error": "Errore nel comando diretto: {error}"
|
||||
},
|
||||
"update_dialog": {
|
||||
"title": "Aggiornamento disponibile",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Prossimo controllo: {time}",
|
||||
"next_check_error": "Prossimo controllo: Errore di calcolo",
|
||||
"checking": "🔄 Controllo...",
|
||||
"check_now": "🔍 Controlla aggiornamenti ora"
|
||||
"check_now": "🔍 Controlla aggiornamenti ora",
|
||||
"current_version": "Versione corrente di yt-dlp: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "L'URL non può essere vuoto",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "Sei sicuro? Questa azione non può essere annullata.",
|
||||
"no_history": "Nessuna cronologia ancora",
|
||||
"no_history_description": "I tuoi download appariranno qui",
|
||||
"loading": "Caricamento cronologia...",
|
||||
"search_placeholder": "Cerca...",
|
||||
"open_location": "Apri Posizione",
|
||||
"redownload": "Scarica di Nuovo",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 download",
|
||||
"redownload_confirm_title": "Scaricare di Nuovo?",
|
||||
"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": {
|
||||
"title": "Controllo Versione FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "音声のみ"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "リセット",
|
||||
"download": "ダウンロード",
|
||||
"pause": "一時停止",
|
||||
"resume": "再開",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "字幕を選択",
|
||||
"filter_languages_placeholder": "言語でフィルタ (例: ja, en)...",
|
||||
"no_subtitles_available": "利用可能な字幕がありません",
|
||||
"matching": "一致"
|
||||
"matching": "一致",
|
||||
"ytdlp_log_title": "yt-dlpログ"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Cookieでログイン",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "ブラウザCookieが適用されました",
|
||||
"browser_applied_message": "ブラウザCookieが抽出されます: {browser}",
|
||||
"cleared_title": "Cookieがクリアされました",
|
||||
"cleared_message": "Cookie設定がクリアされました"
|
||||
"cleared_message": "Cookie設定がクリアされました",
|
||||
"active_browser": "✓ 有効: ブラウザーのCookie ({browser})",
|
||||
"active_file": "✓ 有効: Cookieファイル ({file})",
|
||||
"none_active": "○ 有効なCookieはありません"
|
||||
},
|
||||
"custom_command": {
|
||||
"help_text": "以下にカスタムyt-dlpコマンドを入力してください。現在のURLは自動的に追加されます。<br><br>オプションの完全なリストと使用例については、<a href=\"{docs_url}\">こちらをクリックしてyt-dlp公式ドキュメントを参照してください</a>。<br><br>注: ダウンロードパスとファイル名テンプレートは自動的に処理されます。",
|
||||
@@ -137,7 +142,14 @@
|
||||
"full_command": "🔧 完全なコマンド: {command}",
|
||||
"command_success": "✅ カスタムコマンドが正常に実行されました!",
|
||||
"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": {
|
||||
"help_text": "ダウンロード用のプロキシ設定を構成します。直接接続の場合は空白のままにしてください。",
|
||||
@@ -159,7 +171,15 @@
|
||||
"invalid_main_url": "メインプロキシのURL形式が無効です",
|
||||
"invalid_geo_url": "地域プロキシのURL形式が無効です",
|
||||
"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": {
|
||||
"preparing": "ダウンロードを準備中...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "システム情報",
|
||||
"loading": "🔄 システム情報を読み込み中...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 ログ",
|
||||
"logs_tooltip": "アプリケーションのログフォルダを開く",
|
||||
"refreshing": "🔄 更新中...",
|
||||
"refresh_failed": "更新に失敗しました",
|
||||
"refresh_failed_message": "バージョン情報を更新できませんでした。",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ {channel}チャンネルへの切り替えに成功しました!",
|
||||
"ytdlp_channel_switch_failed": "❌ チャンネルの切り替えに失敗しました: {error}",
|
||||
"ytdlp_current_channel": "現在のチャンネル: {channel}",
|
||||
"app_updates_title": "YTSageの更新",
|
||||
"check_beta_updates": "ベータ版の更新を受け取る",
|
||||
"auto_update_title": "自動更新設定",
|
||||
"auto_update_header": "🔄 自動更新設定",
|
||||
"auto_update_description": "yt-dlpの自動更新を設定して、最新の機能とバグ修正を確実に入手してください。",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV(非圧縮)",
|
||||
"audio_format_opus": "Opus(効率的)",
|
||||
"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": {
|
||||
"url_placeholder": "YouTubeの動画URLまたはプレイリストURLを入力",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "ブラウザCookieが抽出されます: {browser}",
|
||||
"error_no_format_info": "エラー: 利用可能なフォーマット情報がありません。",
|
||||
"error_extract_info": "エラー: 基本的な動画情報を抽出できませんでした。リンクを確認してください。",
|
||||
"analyzing_preparing": "解析中 (0%)... リクエストを準備中",
|
||||
"analyzing_extracting_basic": "解析中 (15%)... 基本情報を抽出中",
|
||||
"analyzing_extracting_detailed": "解析中 (30%)... 詳細情報を抽出中",
|
||||
"analyzing_processing_video": "解析中 (45%)... 動画データを処理中",
|
||||
"analyzing_processing_formats": "解析中 (60%)... フォーマットを処理中",
|
||||
"analyzing_loading_thumbnail": "解析中 (75%)... サムネイルを読み込み中",
|
||||
"analyzing_processing_subtitles": "解析中 (85%)... 字幕を処理中",
|
||||
"analyzing_updating_table": "解析中 (95%)... フォーマットテーブルを更新中",
|
||||
"analyzing_preparing": "リクエストを準備中...",
|
||||
"analyzing_extracting_basic": "基本情報を抽出中...",
|
||||
"analyzing_extracting_detailed": "詳細情報を抽出中...",
|
||||
"analyzing_processing_video": "動画データを処理中...",
|
||||
"analyzing_processing_formats": "フォーマットを処理中...",
|
||||
"analyzing_loading_thumbnail": "サムネイルを読み込み中...",
|
||||
"analyzing_processing_subtitles": "字幕を処理中...",
|
||||
"analyzing_updating_table": "フォーマットテーブルを更新中...",
|
||||
"analysis_complete": "解析完了!",
|
||||
"analyzing_extracting_ytdlp": "解析中 (30%)... 情報を抽出中",
|
||||
"analyzing_processing_data": "解析中 (60%)... データを処理中",
|
||||
"analyzing_processing_formats_ytdlp": "解析中 (75%)... フォーマットを処理中",
|
||||
"analyzing_loading_thumbnail_ytdlp": "解析中 (85%)... サムネイルを読み込み中",
|
||||
"analyzing_processing_subtitles_ytdlp": "解析中 (90%)... 字幕を処理中",
|
||||
"analyzing_extracting_ytdlp": "情報を抽出中...",
|
||||
"analyzing_fetching_first_video": "最初のビデオの形式を取得しています...",
|
||||
"analyzing_processing_data": "データを処理中...",
|
||||
"analyzing_processing_formats_ytdlp": "フォーマットを処理中...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "サムネイルを読み込み中...",
|
||||
"analyzing_processing_subtitles_ytdlp": "字幕を処理中...",
|
||||
"select_subtitles": "字幕を選択...",
|
||||
"sponsorblock_categories": "SponsorBlockカテゴリ...",
|
||||
"invalid_url_or_enter": "無効なURLまたはURLを入力してください。",
|
||||
"zero_selected": "0個選択",
|
||||
"analyze_first_tooltip": "最初に動画を分析してください",
|
||||
"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": {
|
||||
"sponsor": "スポンサー",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "エラー: yt-dlpが失敗しました: {error}",
|
||||
"parse_failed": "エラー: yt-dlp出力の解析に失敗しました: {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": {
|
||||
"title": "アップデートが利用可能です",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "次回確認: {time}",
|
||||
"next_check_error": "次回確認: 計算エラー",
|
||||
"checking": "🔄 確認中...",
|
||||
"check_now": "🔍 今すぐ更新を確認"
|
||||
"check_now": "🔍 今すぐ更新を確認",
|
||||
"current_version": "現在のyt-dlpバージョン: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "URLを空にすることはできません",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "本当によろしいですか?この操作は取り消せません。",
|
||||
"no_history": "まだ履歴がありません",
|
||||
"no_history_description": "ダウンロードがここに表示されます",
|
||||
"loading": "履歴を読み込み中...",
|
||||
"search_placeholder": "検索...",
|
||||
"open_location": "場所を開く",
|
||||
"redownload": "再ダウンロード",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 件",
|
||||
"redownload_confirm_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": {
|
||||
"title": "FFmpegバージョンチェッカー",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Tylko dźwięk"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Zresetuj",
|
||||
"download": "Pobierz",
|
||||
"pause": "Wstrzymaj",
|
||||
"resume": "Wznów",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Wybierz napisy",
|
||||
"filter_languages_placeholder": "Filtruj języki (np: pl, en)...",
|
||||
"no_subtitles_available": "Brak dostępnych napisów",
|
||||
"matching": "dopasowujące"
|
||||
"matching": "dopasowujące",
|
||||
"ytdlp_log_title": "Log yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Zaloguj za pomocą ciasteczek",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Zastosowano ciasteczka przeglądarki",
|
||||
"browser_applied_message": "Ciasteczka przeglądarki zostaną wyodrębnione z: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Polecenie niestandardowe wykonane pomyślnie!",
|
||||
"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": {
|
||||
"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_geo_url": "Nieprawidłowy format URL proxy geograficznego",
|
||||
"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": {
|
||||
"preparing": "Przygotowywanie pobierania...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Informacje systemowe",
|
||||
"loading": "🔄 Ładowanie informacji systemowych...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Logi",
|
||||
"logs_tooltip": "Otwórz folder logów aplikacji",
|
||||
"refreshing": "🔄 Odświeżanie...",
|
||||
"refresh_failed": "Odświeżanie nie powiodło się",
|
||||
"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_switch_failed": "❌ Nie udało się przełączyć kanału: {error}",
|
||||
"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_header": "🔄 Ustawienia automatycznych aktualizacji",
|
||||
"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_opus": "Opus (Wydajny)",
|
||||
"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": {
|
||||
"url_placeholder": "Wprowadź URL wideo YouTube lub playlisty",
|
||||
@@ -326,27 +352,32 @@
|
||||
"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_extract_info": "Błąd: Nie można wyodrębnić podstawowych informacji o wideo. Sprawdź swój link.",
|
||||
"analyzing_preparing": "Analizowanie (0%)... Przygotowywanie żądania",
|
||||
"analyzing_extracting_basic": "Analizowanie (15%)... Wyodrębnianie podstawowych informacji",
|
||||
"analyzing_extracting_detailed": "Analizowanie (30%)... Wyodrębnianie szczegółowych informacji",
|
||||
"analyzing_processing_video": "Analizowanie (45%)... Przetwarzanie danych wideo",
|
||||
"analyzing_processing_formats": "Analizowanie (60%)... Przetwarzanie formatów",
|
||||
"analyzing_loading_thumbnail": "Analizowanie (75%)... Ładowanie miniatury",
|
||||
"analyzing_processing_subtitles": "Analizowanie (85%)... Przetwarzanie napisów",
|
||||
"analyzing_updating_table": "Analizowanie (95%)... Aktualizowanie tabeli formatów",
|
||||
"analyzing_preparing": "Przygotowywanie żądania...",
|
||||
"analyzing_extracting_basic": "Wyodrębnianie podstawowych informacji...",
|
||||
"analyzing_extracting_detailed": "Wyodrębnianie szczegółowych informacji...",
|
||||
"analyzing_processing_video": "Przetwarzanie danych wideo...",
|
||||
"analyzing_processing_formats": "Przetwarzanie formatów...",
|
||||
"analyzing_loading_thumbnail": "Ładowanie miniatury...",
|
||||
"analyzing_processing_subtitles": "Przetwarzanie napisów...",
|
||||
"analyzing_updating_table": "Aktualizowanie tabeli formatów...",
|
||||
"analysis_complete": "Analiza zakończona!",
|
||||
"analyzing_extracting_ytdlp": "Analizowanie (30%)... Wyodrębnianie informacji",
|
||||
"analyzing_processing_data": "Analizowanie (60%)... Przetwarzanie danych",
|
||||
"analyzing_processing_formats_ytdlp": "Analizowanie (75%)... Przetwarzanie formatów",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analizowanie (85%)... Ładowanie miniatury",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analizowanie (90%)... Przetwarzanie napisów",
|
||||
"analyzing_fetching_first_video": "Pobieranie formatów dla pierwszego filmu...",
|
||||
"analyzing_extracting_ytdlp": "Wyodrębnianie informacji...",
|
||||
"analyzing_processing_data": "Przetwarzanie danych...",
|
||||
"analyzing_processing_formats_ytdlp": "Przetwarzanie formatów...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Ładowanie miniatury...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Przetwarzanie napisów...",
|
||||
"select_subtitles": "Wybierz napisy...",
|
||||
"sponsorblock_categories": "Kategorie SponsorBlock...",
|
||||
"invalid_url_or_enter": "Nieprawidłowy URL lub wprowadź URL.",
|
||||
"zero_selected": "0 wybranych",
|
||||
"analyze_first_tooltip": "Najpierw przeanalizuj wideo",
|
||||
"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": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +439,10 @@
|
||||
"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}",
|
||||
"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": {
|
||||
"title": "Dostępna aktualizacja",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Następne sprawdzenie: {time}",
|
||||
"next_check_error": "Następne sprawdzenie: Błąd obliczania",
|
||||
"checking": "🔄 Sprawdzanie...",
|
||||
"check_now": "🔍 Sprawdź aktualizacje teraz"
|
||||
"check_now": "🔍 Sprawdź aktualizacje teraz",
|
||||
"current_version": "Bieżąca wersja yt-dlp: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "URL nie może być pusty",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "Czy jesteś pewien? Nie można tego cofnąć.",
|
||||
"no_history": "Brak historii",
|
||||
"no_history_description": "Twoje pobrane pliki pojawią się tutaj",
|
||||
"loading": "Ładowanie historii...",
|
||||
"search_placeholder": "Szukaj...",
|
||||
"open_location": "Otwórz Lokalizację",
|
||||
"redownload": "Pobierz Ponownie",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 pobranie",
|
||||
"redownload_confirm_title": "Pobrać Ponownie?",
|
||||
"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": {
|
||||
"title": "Sprawdzanie Wersji FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Apenas áudio"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Redefinir",
|
||||
"download": "Baixar",
|
||||
"pause": "Pausar",
|
||||
"resume": "Retomar",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Selecionar Legendas",
|
||||
"filter_languages_placeholder": "Filtrar idiomas (ex., en, pt)...",
|
||||
"no_subtitles_available": "Nenhuma legenda disponível",
|
||||
"matching": "correspondendo"
|
||||
"matching": "correspondendo",
|
||||
"ytdlp_log_title": "Log do yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Entrar com Cookies",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Cookies do Navegador Aplicados",
|
||||
"browser_applied_message": "Os cookies do navegador serão extraídos de: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Comando personalizado executado com sucesso!",
|
||||
"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": {
|
||||
"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_geo_url": "Formato de URL do proxy geográfico inválido",
|
||||
"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": {
|
||||
"preparing": "Preparando download...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Informações do Sistema",
|
||||
"loading": "🔄 Carregando informações do sistema...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Logs",
|
||||
"logs_tooltip": "Abrir pasta de logs da aplicação",
|
||||
"refreshing": "🔄 Atualizando...",
|
||||
"refresh_failed": "Atualização Falhou",
|
||||
"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_switch_failed": "❌ Falha ao trocar de canal: {error}",
|
||||
"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_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.",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV (Não comprimido)",
|
||||
"audio_format_opus": "Opus (Eficiente)",
|
||||
"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": {
|
||||
"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}",
|
||||
"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.",
|
||||
"analyzing_preparing": "Analisando (0%)... Preparando solicitação",
|
||||
"analyzing_extracting_basic": "Analisando (15%)... Extraindo informações básicas",
|
||||
"analyzing_extracting_detailed": "Analisando (30%)... Extraindo informações detalhadas",
|
||||
"analyzing_processing_video": "Analisando (45%)... Processando dados do vídeo",
|
||||
"analyzing_processing_formats": "Analisando (60%)... Processando formatos",
|
||||
"analyzing_loading_thumbnail": "Analisando (75%)... Carregando miniatura",
|
||||
"analyzing_processing_subtitles": "Analisando (85%)... Processando legendas",
|
||||
"analyzing_updating_table": "Analisando (95%)... Atualizando tabela de formatos",
|
||||
"analyzing_preparing": "Preparando solicitação...",
|
||||
"analyzing_extracting_basic": "Extraindo informações básicas...",
|
||||
"analyzing_extracting_detailed": "Extraindo informações detalhadas...",
|
||||
"analyzing_processing_video": "Processando dados do vídeo...",
|
||||
"analyzing_processing_formats": "Processando formatos...",
|
||||
"analyzing_loading_thumbnail": "Carregando miniatura...",
|
||||
"analyzing_processing_subtitles": "Processando legendas...",
|
||||
"analyzing_updating_table": "Atualizando tabela de formatos...",
|
||||
"analysis_complete": "Análise completa!",
|
||||
"analyzing_extracting_ytdlp": "Analisando (30%)... Extraindo informações",
|
||||
"analyzing_processing_data": "Analisando (60%)... Processando dados",
|
||||
"analyzing_processing_formats_ytdlp": "Analisando (75%)... Processando formatos",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analisando (85%)... Carregando miniatura",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analisando (90%)... Processando legendas",
|
||||
"analyzing_fetching_first_video": "Obtendo formatos do primeiro vídeo...",
|
||||
"analyzing_extracting_ytdlp": "Extraindo informações...",
|
||||
"analyzing_processing_data": "Processando dados...",
|
||||
"analyzing_processing_formats_ytdlp": "Processando formatos...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Carregando miniatura...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Processando legendas...",
|
||||
"select_subtitles": "Selecionar Legendas...",
|
||||
"sponsorblock_categories": "Categorias SponsorBlock...",
|
||||
"invalid_url_or_enter": "URL inválido ou por favor insira um URL.",
|
||||
"zero_selected": "0 selecionados",
|
||||
"analyze_first_tooltip": "Por favor analise o vídeo primeiro",
|
||||
"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": {
|
||||
"sponsor": "Patrocinador",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "Erro: yt-dlp falhou: {error}",
|
||||
"parse_failed": "Erro: Falha ao analisar a saída do yt-dlp: {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": {
|
||||
"title": "Atualização Disponível",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Próxima verificação: {time}",
|
||||
"next_check_error": "Próxima verificação: Erro ao calcular",
|
||||
"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": {
|
||||
"empty_url": "O URL não pode estar vazio",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "Tem certeza? Isto não pode ser desfeito.",
|
||||
"no_history": "Nenhum histórico ainda",
|
||||
"no_history_description": "Seus downloads aparecerão aqui",
|
||||
"loading": "Carregando histórico...",
|
||||
"search_placeholder": "Pesquisar...",
|
||||
"open_location": "Abrir Local",
|
||||
"redownload": "Baixar Novamente",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 download",
|
||||
"redownload_confirm_title": "Baixar Novamente?",
|
||||
"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": {
|
||||
"title": "Verificador de Versão FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Только аудио"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Сброс",
|
||||
"download": "Скачать",
|
||||
"pause": "Пауза",
|
||||
"resume": "Продолжить",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Выбрать субтитры",
|
||||
"filter_languages_placeholder": "Фильтр языков (например, en, ru)...",
|
||||
"no_subtitles_available": "Субтитры недоступны",
|
||||
"matching": "соответствующие"
|
||||
"matching": "соответствующие",
|
||||
"ytdlp_log_title": "Журнал yt-dlp"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Войти через Cookie",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Cookie браузера применены",
|
||||
"browser_applied_message": "Cookie браузера будут извлечены из: {browser}",
|
||||
"cleared_title": "Cookie очищены",
|
||||
"cleared_message": "Настройки cookie были очищены"
|
||||
"cleared_message": "Настройки cookie были очищены",
|
||||
"active_browser": "✓ Активны: cookie браузера ({browser})",
|
||||
"active_file": "✓ Активен: файл cookie ({file})",
|
||||
"none_active": "○ Нет активных cookie"
|
||||
},
|
||||
"custom_command": {
|
||||
"help_text": "Введите пользовательскую команду yt-dlp ниже. Текущий URL будет добавлен автоматически.<br><br>Для полного списка опций и примеров использования, <a href=\"{docs_url}\">нажмите здесь для просмотра официальной документации yt-dlp</a>.<br><br>Примечание: Путь загрузки и шаблон имени файла будут обработаны автоматически.",
|
||||
@@ -137,7 +142,14 @@
|
||||
"full_command": "🔧 Полная команда: {command}",
|
||||
"command_success": "✅ Пользовательская команда выполнена успешно!",
|
||||
"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": {
|
||||
"help_text": "Настройте параметры прокси для загрузки. Оставьте пустым для прямого подключения.",
|
||||
@@ -159,7 +171,15 @@
|
||||
"invalid_main_url": "Неверный формат URL основного прокси",
|
||||
"invalid_geo_url": "Неверный формат URL гео-прокси",
|
||||
"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": {
|
||||
"preparing": "Подготовка загрузки...",
|
||||
@@ -225,6 +245,8 @@
|
||||
"system_info": "Системная информация",
|
||||
"loading": "🔄 Загрузка системной информации...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Логи",
|
||||
"logs_tooltip": "Открыть папку с логами приложения",
|
||||
"refreshing": "🔄 Обновление...",
|
||||
"refresh_failed": "Обновление не удалось",
|
||||
"refresh_failed_message": "Не удалось обновить информацию о версии.",
|
||||
@@ -277,6 +299,8 @@
|
||||
"ytdlp_channel_switched": "✅ Успешно переключено на канал {channel}!",
|
||||
"ytdlp_channel_switch_failed": "❌ Не удалось переключить канал: {error}",
|
||||
"ytdlp_current_channel": "Текущий канал: {channel}",
|
||||
"app_updates_title": "Обновления YTSage",
|
||||
"check_beta_updates": "Получать бета-обновления",
|
||||
"auto_update_title": "Настройки автообновления",
|
||||
"auto_update_header": "🔄 Настройки автообновления",
|
||||
"auto_update_description": "Настройте автоматические обновления для yt-dlp, чтобы всегда иметь последние функции и исправления ошибок.",
|
||||
@@ -307,7 +331,9 @@
|
||||
"audio_format_wav": "WAV (Несжатый)",
|
||||
"audio_format_opus": "Opus (Эффективный)",
|
||||
"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": {
|
||||
"url_placeholder": "Введите URL видео или плейлиста YouTube",
|
||||
@@ -326,27 +352,32 @@
|
||||
"browser_cookies_selected_message": "Cookie браузера будут извлечены из: {browser}",
|
||||
"error_no_format_info": "Ошибка: Информация о формате недоступна.",
|
||||
"error_extract_info": "Ошибка: Не удалось извлечь базовую информацию о видео. Проверьте вашу ссылку.",
|
||||
"analyzing_preparing": "Анализ (0%)... Подготовка запроса",
|
||||
"analyzing_extracting_basic": "Анализ (15%)... Извлечение базовой информации",
|
||||
"analyzing_extracting_detailed": "Анализ (30%)... Извлечение подробной информации",
|
||||
"analyzing_processing_video": "Анализ (45%)... Обработка данных видео",
|
||||
"analyzing_processing_formats": "Анализ (60%)... Обработка форматов",
|
||||
"analyzing_loading_thumbnail": "Анализ (75%)... Загрузка миниатюры",
|
||||
"analyzing_processing_subtitles": "Анализ (85%)... Обработка субтитров",
|
||||
"analyzing_updating_table": "Анализ (95%)... Обновление таблицы форматов",
|
||||
"analyzing_preparing": "Подготовка запроса...",
|
||||
"analyzing_extracting_basic": "Извлечение базовой информации...",
|
||||
"analyzing_extracting_detailed": "Извлечение подробной информации...",
|
||||
"analyzing_processing_video": "Обработка данных видео...",
|
||||
"analyzing_processing_formats": "Обработка форматов...",
|
||||
"analyzing_loading_thumbnail": "Загрузка миниатюры...",
|
||||
"analyzing_processing_subtitles": "Обработка субтитров...",
|
||||
"analyzing_updating_table": "Обновление таблицы форматов...",
|
||||
"analysis_complete": "Анализ завершен!",
|
||||
"analyzing_extracting_ytdlp": "Анализ (30%)... Извлечение информации",
|
||||
"analyzing_processing_data": "Анализ (60%)... Обработка данных",
|
||||
"analyzing_processing_formats_ytdlp": "Анализ (75%)... Обработка форматов",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Анализ (85%)... Загрузка миниатюры",
|
||||
"analyzing_processing_subtitles_ytdlp": "Анализ (90%)... Обработка субтитров",
|
||||
"analyzing_fetching_first_video": "Получение форматов для первого видео...",
|
||||
"analyzing_extracting_ytdlp": "Извлечение информации...",
|
||||
"analyzing_processing_data": "Обработка данных...",
|
||||
"analyzing_processing_formats_ytdlp": "Обработка форматов...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Загрузка миниатюры...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Обработка субтитров...",
|
||||
"select_subtitles": "Выбрать субтитры...",
|
||||
"sponsorblock_categories": "Категории SponsorBlock...",
|
||||
"invalid_url_or_enter": "Неверный URL или пожалуйста введите URL.",
|
||||
"zero_selected": "0 выбрано",
|
||||
"analyze_first_tooltip": "Пожалуйста, сначала проанализируйте видео",
|
||||
"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": {
|
||||
"sponsor": "Спонсор",
|
||||
@@ -408,7 +439,10 @@
|
||||
"ytdlp_failed": "Ошибка: yt-dlp завершился с ошибкой: {error}",
|
||||
"parse_failed": "Ошибка: Не удалось разобрать вывод yt-dlp: {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": {
|
||||
"title": "Доступно обновление",
|
||||
@@ -442,7 +476,8 @@
|
||||
"next_check": "Следующая проверка: {time}",
|
||||
"next_check_error": "Следующая проверка: Ошибка расчета",
|
||||
"checking": "🔄 Проверка...",
|
||||
"check_now": "🔍 Проверить обновления сейчас"
|
||||
"check_now": "🔍 Проверить обновления сейчас",
|
||||
"current_version": "Текущая версия yt-dlp: {version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "URL не может быть пустым",
|
||||
@@ -473,6 +508,7 @@
|
||||
"clear_confirm_message": "Вы уверены? Это нельзя отменить.",
|
||||
"no_history": "История пока пуста",
|
||||
"no_history_description": "Загрузки появятся здесь",
|
||||
"loading": "Загрузка истории...",
|
||||
"search_placeholder": "Поиск...",
|
||||
"open_location": "Открыть Папку",
|
||||
"redownload": "Загрузить Снова",
|
||||
@@ -491,7 +527,47 @@
|
||||
"one_entry": "1 загрузка",
|
||||
"redownload_confirm_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": {
|
||||
"title": "Проверка версии FFmpeg",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "Sadece ses"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "Sıfırla",
|
||||
"download": "İndir",
|
||||
"pause": "Duraklat",
|
||||
"resume": "Devam Et",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "Altyazı seç",
|
||||
"filter_languages_placeholder": "Dilleri filtrele (örn: tr, en)...",
|
||||
"no_subtitles_available": "Altyazı mevcut değil",
|
||||
"matching": "eşleşen"
|
||||
"matching": "eşleşen",
|
||||
"ytdlp_log_title": "yt-dlp Günlüğü"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "Çerezlerle giriş yap",
|
||||
@@ -124,7 +126,10 @@
|
||||
"browser_selected_title": "Tarayıcı Çerezleri Uygulandı",
|
||||
"browser_applied_message": "Tarayıcı çerezleri şuradan çıkarılacak: {browser}",
|
||||
"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": {
|
||||
"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}",
|
||||
"command_success": "✅ Özel komut başarıyla çalıştırıldı!",
|
||||
"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": {
|
||||
"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_geo_url": "Geçersiz coğrafi proxy URL formatı",
|
||||
"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": {
|
||||
"preparing": "İndirme hazırlanıyor...",
|
||||
@@ -225,6 +243,8 @@
|
||||
"system_info": "Sistem bilgileri",
|
||||
"loading": "🔄 Sistem bilgileri yükleniyor...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 Kayıtlar",
|
||||
"logs_tooltip": "Uygulama kayıt klasörünü aç",
|
||||
"refreshing": "🔄 Yenileniyor...",
|
||||
"refresh_failed": "Yenileme başarısız",
|
||||
"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_switch_failed": "❌ Kanal değiştirilemedi: {error}",
|
||||
"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_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.",
|
||||
@@ -307,7 +329,9 @@
|
||||
"audio_format_wav": "WAV (Sıkıştırılmamış)",
|
||||
"audio_format_opus": "Opus (Verimli)",
|
||||
"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": {
|
||||
"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}",
|
||||
"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.",
|
||||
"analyzing_preparing": "Analiz ediliyor (0%)... İstek hazırlanıyor",
|
||||
"analyzing_extracting_basic": "Analiz ediliyor (15%)... Temel bilgiler çıkarılıyor",
|
||||
"analyzing_extracting_detailed": "Analiz ediliyor (30%)... Ayrıntılı bilgiler çıkarılıyor",
|
||||
"analyzing_processing_video": "Analiz ediliyor (45%)... Video verisi işleniyor",
|
||||
"analyzing_processing_formats": "Analiz ediliyor (60%)... Formatlar işleniyor",
|
||||
"analyzing_loading_thumbnail": "Analiz ediliyor (75%)... Küçük resim yükleniyor",
|
||||
"analyzing_processing_subtitles": "Analiz ediliyor (85%)... Altyazılar işleniyor",
|
||||
"analyzing_updating_table": "Analiz ediliyor (95%)... Format tablosu güncelleniyor",
|
||||
"analyzing_preparing": "İstek hazırlanıyor...",
|
||||
"analyzing_extracting_basic": "Temel bilgiler çıkarılıyor...",
|
||||
"analyzing_extracting_detailed": "Ayrıntılı bilgiler çıkarılıyor...",
|
||||
"analyzing_processing_video": "Video verisi işleniyor...",
|
||||
"analyzing_processing_formats": "Formatlar işleniyor...",
|
||||
"analyzing_loading_thumbnail": "Küçük resim yükleniyor...",
|
||||
"analyzing_processing_subtitles": "Altyazılar işleniyor...",
|
||||
"analyzing_updating_table": "Format tablosu güncelleniyor...",
|
||||
"analysis_complete": "Analiz tamamlandı!",
|
||||
"analyzing_extracting_ytdlp": "Analiz ediliyor (30%)... Bilgiler çıkarılıyor",
|
||||
"analyzing_processing_data": "Analiz ediliyor (60%)... Veriler işleniyor",
|
||||
"analyzing_processing_formats_ytdlp": "Analiz ediliyor (75%)... Formatlar işleniyor",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Analiz ediliyor (85%)... Küçük resim yükleniyor",
|
||||
"analyzing_processing_subtitles_ytdlp": "Analiz ediliyor (90%)... Altyazılar işleniyor",
|
||||
"analyzing_fetching_first_video": "İlk video için formatlar alınıyor...",
|
||||
"analyzing_extracting_ytdlp": "Bilgiler çıkarılıyor...",
|
||||
"analyzing_processing_data": "Veriler işleniyor...",
|
||||
"analyzing_processing_formats_ytdlp": "Formatlar işleniyor...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "Küçük resim yükleniyor...",
|
||||
"analyzing_processing_subtitles_ytdlp": "Altyazılar işleniyor...",
|
||||
"select_subtitles": "Altyazı seç...",
|
||||
"sponsorblock_categories": "SponsorBlock kategorileri...",
|
||||
"invalid_url_or_enter": "Geçersiz URL veya lütfen bir URL girin.",
|
||||
"zero_selected": "0 seçildi",
|
||||
"analyze_first_tooltip": "Lütfen önce videoyu analiz edin",
|
||||
"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": {
|
||||
"sponsor": "Sponsor",
|
||||
@@ -408,7 +437,10 @@
|
||||
"ytdlp_failed": "Hata: yt-dlp başarısız oldu: {error}",
|
||||
"parse_failed": "Hata: yt-dlp çıktısı ayrıştırılamadı: {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": {
|
||||
"title": "Güncelleme Mevcut",
|
||||
@@ -442,7 +474,8 @@
|
||||
"next_check": "Sonraki kontrol: {time}",
|
||||
"next_check_error": "Sonraki kontrol: Hesaplama hatası",
|
||||
"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": {
|
||||
"empty_url": "URL boş olamaz",
|
||||
@@ -491,7 +524,41 @@
|
||||
"one_entry": "1 indirme",
|
||||
"redownload_confirm_title": "Tekrar İndir?",
|
||||
"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": {
|
||||
"title": "FFmpeg Sürüm Denetçisi",
|
||||
@@ -57,6 +57,7 @@
|
||||
"audio_only_resolution": "仅音频"
|
||||
},
|
||||
"buttons": {
|
||||
"reset": "重置",
|
||||
"download": "下载",
|
||||
"pause": "暂停",
|
||||
"resume": "恢复",
|
||||
@@ -93,7 +94,8 @@
|
||||
"select_subtitles": "选择字幕",
|
||||
"filter_languages_placeholder": "过滤语言(例如:en, zh)...",
|
||||
"no_subtitles_available": "无可用字幕",
|
||||
"matching": "匹配"
|
||||
"matching": "匹配",
|
||||
"ytdlp_log_title": "yt-dlp 日志"
|
||||
},
|
||||
"tabs": {
|
||||
"cookies": "使用 Cookie 登录",
|
||||
@@ -159,7 +161,13 @@
|
||||
"invalid_main_url": "主代理网址格式无效",
|
||||
"invalid_geo_url": "地理代理网址格式无效",
|
||||
"main_configured": "主代理已配置",
|
||||
"geo_configured": "地理代理已配置"
|
||||
"geo_configured": "地理代理已配置",
|
||||
"set_title": "已设置代理",
|
||||
"set_message": "主代理已设置并保存: {proxy}",
|
||||
"geo_set_title": "已设置地理代理",
|
||||
"geo_set_message": "地理验证代理已设置并保存: {proxy}",
|
||||
"cleared_title": "代理设置已清除",
|
||||
"cleared_message": "所有代理设置已清除并保存。"
|
||||
},
|
||||
"download": {
|
||||
"preparing": "正在准备下载...",
|
||||
@@ -225,6 +233,8 @@
|
||||
"system_info": "系统信息",
|
||||
"loading": "🔄 正在加载系统信息...",
|
||||
"refresh": "🔄",
|
||||
"open_logs": "📂 日志",
|
||||
"logs_tooltip": "打开应用程序日志文件夹",
|
||||
"refreshing": "🔄 正在刷新...",
|
||||
"refresh_failed": "刷新失败",
|
||||
"refresh_failed_message": "无法刷新版本信息。",
|
||||
@@ -277,6 +287,8 @@
|
||||
"ytdlp_channel_switched": "✅ 成功切换到 {channel} 渠道!",
|
||||
"ytdlp_channel_switch_failed": "❌ 切换渠道失败:{error}",
|
||||
"ytdlp_current_channel": "当前渠道:{channel}",
|
||||
"app_updates_title": "YTSage 更新",
|
||||
"check_beta_updates": "接收测试版更新",
|
||||
"auto_update_title": "自动更新设置",
|
||||
"auto_update_header": "🔄 自动更新设置",
|
||||
"auto_update_description": "为 yt-dlp 配置自动更新,以确保您始终拥有最新功能和错误修复。",
|
||||
@@ -307,7 +319,9 @@
|
||||
"audio_format_wav": "WAV(未压缩)",
|
||||
"audio_format_opus": "Opus(高效)",
|
||||
"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": {
|
||||
"url_placeholder": "输入 YouTube 视频或播放列表网址",
|
||||
@@ -326,27 +340,32 @@
|
||||
"browser_cookies_selected_message": "将从以下浏览器提取 Cookie:{browser}",
|
||||
"error_no_format_info": "错误:无可用格式信息。",
|
||||
"error_extract_info": "错误:无法提取基本视频信息。请检查您的链接。",
|
||||
"analyzing_preparing": "分析中 (0%)... 正在准备请求",
|
||||
"analyzing_extracting_basic": "分析中 (15%)... 正在提取基本信息",
|
||||
"analyzing_extracting_detailed": "分析中 (30%)... 正在提取详细信息",
|
||||
"analyzing_processing_video": "分析中 (45%)... 正在处理视频数据",
|
||||
"analyzing_processing_formats": "分析中 (60%)... 正在处理格式",
|
||||
"analyzing_loading_thumbnail": "分析中 (75%)... 正在加载缩略图",
|
||||
"analyzing_processing_subtitles": "分析中 (85%)... 正在处理字幕",
|
||||
"analyzing_updating_table": "分析中 (95%)... 正在更新格式表",
|
||||
"analyzing_preparing": "正在准备请求...",
|
||||
"analyzing_extracting_basic": "正在提取基本信息...",
|
||||
"analyzing_extracting_detailed": "正在提取详细信息...",
|
||||
"analyzing_processing_video": "正在处理视频数据...",
|
||||
"analyzing_processing_formats": "正在处理格式...",
|
||||
"analyzing_loading_thumbnail": "正在加载缩略图...",
|
||||
"analyzing_processing_subtitles": "正在处理字幕...",
|
||||
"analyzing_updating_table": "正在更新格式表...",
|
||||
"analysis_complete": "分析完成!",
|
||||
"analyzing_extracting_ytdlp": "分析中 (30%)... 提取信息",
|
||||
"analyzing_processing_data": "分析中 (60%)... 正在处理数据",
|
||||
"analyzing_processing_formats_ytdlp": "分析中 (75%)... 正在处理格式",
|
||||
"analyzing_loading_thumbnail_ytdlp": "分析中 (85%)... 正在加载缩略图",
|
||||
"analyzing_processing_subtitles_ytdlp": "分析中 (90%)... 正在处理字幕",
|
||||
"analyzing_fetching_first_video": "正在获取第一个视频的格式...",
|
||||
"analyzing_extracting_ytdlp": "提取信息...",
|
||||
"analyzing_processing_data": "正在处理数据...",
|
||||
"analyzing_processing_formats_ytdlp": "正在处理格式...",
|
||||
"analyzing_loading_thumbnail_ytdlp": "正在加载缩略图...",
|
||||
"analyzing_processing_subtitles_ytdlp": "正在处理字幕...",
|
||||
"select_subtitles": "选择字幕...",
|
||||
"sponsorblock_categories": "SponsorBlock 分类...",
|
||||
"invalid_url_or_enter": "无效的URL或请输入URL。",
|
||||
"zero_selected": "已选择 0 个",
|
||||
"analyze_first_tooltip": "请先分析视频",
|
||||
"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": {
|
||||
"sponsor": "赞助商",
|
||||
@@ -408,7 +427,10 @@
|
||||
"ytdlp_failed": "错误:yt-dlp失败:{error}",
|
||||
"parse_failed": "错误:解析yt-dlp输出失败:{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": {
|
||||
"title": "有可用更新",
|
||||
@@ -442,7 +464,8 @@
|
||||
"next_check": "下次检查: {time}",
|
||||
"next_check_error": "下次检查: 计算错误",
|
||||
"checking": "🔄 检查中...",
|
||||
"check_now": "🔍 立即检查更新"
|
||||
"check_now": "🔍 立即检查更新",
|
||||
"current_version": "当前 yt-dlp 版本:{version}"
|
||||
},
|
||||
"url_validation": {
|
||||
"empty_url": "URL不能为空",
|
||||
@@ -491,7 +514,41 @@
|
||||
"one_entry": "1 个下载",
|
||||
"redownload_confirm_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": {
|
||||
"title": "FFmpeg 版本检查器",
|
||||
@@ -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()
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Utility modules for YTSage.
|
||||
|
||||
This package contains shared utilities, constants, logging, and configuration management.
|
||||
"""
|
||||
@@ -20,7 +20,7 @@ Features
|
||||
|
||||
Usage
|
||||
-----
|
||||
from src.utils.ytsage_config_manager import ConfigManager
|
||||
from .ytsage_config_manager import ConfigManager
|
||||
|
||||
# Load settings (auto-loads if not already loaded)
|
||||
download_path = ConfigManager.get("download_path")
|
||||
@@ -54,8 +54,8 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from src.utils.ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR
|
||||
from src.utils.ytsage_logger import logger
|
||||
from .ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR
|
||||
from .ytsage_logger import logger
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
@@ -83,6 +83,7 @@ class ConfigManager:
|
||||
"geo_proxy_url": None,
|
||||
"auto_update_ytdlp": True,
|
||||
"auto_update_frequency": "daily",
|
||||
"check_beta_updates": False,
|
||||
"last_update_check": 0,
|
||||
"language": "en",
|
||||
"ytdlp_channel": "stable",
|
||||
@@ -90,6 +91,7 @@ class ConfigManager:
|
||||
"preferred_output_format": "mp4",
|
||||
"force_audio_format": False,
|
||||
"preferred_audio_format": "best",
|
||||
"filename_format": "%(title)s_%(resolution)s.%(ext)s",
|
||||
"cached_versions": {
|
||||
"ytdlp": {"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)
|
||||
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
|
||||
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_ZIP_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.ver"
|
||||
|
||||
# =============================================================================
|
||||
# File Extension Constants
|
||||
# =============================================================================
|
||||
# Centralized file extension definitions to avoid duplication across modules
|
||||
# Use these constants for file type detection throughout the application
|
||||
|
||||
# Video file extensions (container formats that typically contain video)
|
||||
VIDEO_EXTENSIONS: frozenset[str] = frozenset({
|
||||
".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"
|
||||
})
|
||||
|
||||
# Audio file extensions (audio-only formats)
|
||||
AUDIO_EXTENSIONS: frozenset[str] = frozenset({
|
||||
".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"
|
||||
})
|
||||
|
||||
# Subtitle file extensions
|
||||
SUBTITLE_EXTENSIONS: frozenset[str] = frozenset({
|
||||
".vtt", ".srt", ".ass", ".ssa"
|
||||
})
|
||||
|
||||
# Combined video and audio extensions (for file search operations)
|
||||
MEDIA_EXTENSIONS: frozenset[str] = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If this file is run directly, print directory information; if imported, create the necessary directories for the application.
|
||||
# for debug, to check os specific variable which can be different based on os.
|
||||
@@ -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
|
||||
-----
|
||||
from src.utils.ytsage_localization import LocalizationManager
|
||||
from .ytsage_localization import LocalizationManager
|
||||
|
||||
# Get localized text
|
||||
text = LocalizationManager.get_text("download.ready")
|
||||
@@ -33,7 +33,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.utils.ytsage_logger import logger
|
||||
from .ytsage_logger import logger
|
||||
|
||||
|
||||
class LocalizationManager:
|
||||
@@ -46,7 +46,7 @@ class LocalizationManager:
|
||||
_lock = threading.RLock()
|
||||
_current_language = "en"
|
||||
_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_strings = {
|
||||
@@ -108,6 +108,16 @@ class LocalizationManager:
|
||||
},
|
||||
"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 src.utils.ytsage_constants import APP_LOG_DIR, IS_FROZEN
|
||||
from .ytsage_constants import APP_LOG_DIR, IS_FROZEN
|
||||
|
||||
# Separate configs for each handler
|
||||
CONSOLE_CONFIG = {
|
||||