Merge YTSage 5.2.0 history as upstream base for SageTube

Imports the full commit history of github.com/oop7/YTSage (MIT).
LICENSE keeps both copyright lines; upstream README preserved at
docs/UPSTREAM_README.md. Future syncs: git fetch upstream && git merge
upstream/main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-25 01:19:17 +02:00
87 changed files with 35950 additions and 13 deletions
+113
View File
@@ -0,0 +1,113 @@
# YTSage CI/CD Workflow
This repository uses GitHub Actions to automatically build and release YTSage for multiple platforms. The workflow is designed to be manually triggered, allowing for flexibility in building specific versions or platforms on demand.
## How It Works
### Trigger
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, 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
2. **Builds**: Creates platform-specific executables using cx_Freeze
3. **Packages**: Generates native package formats for each platform
4. **Release**: Creates or updates a draft GitHub release with the artifacts
## Usage
### Creating a Full Release (Recommended)
1. Go to the **Actions** tab in the GitHub repository.
2. Select **"Create All Releases"** from the left sidebar.
3. Click **Run workflow**.
4. Enter the **Version name** (e.g., `1.0.0`).
> *Note: Do not include the 'v' prefix in the input field unless you want your filenames to be `v1.0.0`.*
5. Click the green **Run workflow** button.
6. The system will trigger the Windows, Linux, and macOS jobs. Once complete, a draft release will be available in the Releases section.
### Creating a Single Platform Build
1. Go to the **Actions** tab.
2. Select the specific workflow (e.g., **"Build Windows Release"**).
3. Click **Run workflow** and enter the version.
4. Only that specific platform's artifacts will be built and added to the release.
### Release Artifacts
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}-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
- **macOS**: Matrix build for both Intel (x64) and Apple Silicon (arm64)
### Manual Versioning
- Version is strictly controlled by the input you provide at runtime.
- No longer dependent on git tags, reducing accidental releases.
### Caching
- Python dependencies and virtual environments are cached to speed up builds.
### Error Handling
- Comprehensive error checking at each step.
- Artifact verification before upload.
### Security
- Uses official GitHub Actions.
- Secure token handling via `secrets: inherit` for the master workflow.
## Manual Intervention
### After Workflow Completion
1. **Review the draft release** in GitHub.
2. **Test the artifacts** if needed.
3. **Edit release notes** to add changelogs or descriptions.
4. **Publish the release** when ready (change from Draft to Published).
## Configuration
### Modifying the Workflow
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
- `PYTHON_VERSION`: Python version (currently 3.13)
- `version` input: Defined as a required string in all workflows.
## Notes
- All builds use cx_Freeze for packaging.
- FFmpeg binaries are bundled where needed (Windows).
- Screenshots are removed from builds to reduce size.
- Draft releases allow for review before publication.
+72
View File
@@ -0,0 +1,72 @@
---
name: "\U0001F41B Bug Report"
about: Provide a clear and descriptive title
title: ''
labels: bug
assignees: ''
---
## 📝 Description
<!--
Describe the bug in detail. Include:
- Affected feature (e.g., audio extraction, playlist downloads)
- Specific error messages (if any)
-->
## 🔗 Affected Video URL
<!-- Paste the YouTube URL that caused the issue -->
<!-- Example: https://www.youtube.com/watch?v=ABC123XYZ -->
## 🔄 Steps to Reproduce
1. <!-- Step 1 (e.g., "Pasted the URL above into YTSage") -->
2. <!-- Step 2 -->
3. <!-- Step 3 -->
4. <!-- ... -->
## ✅ Expected Behavior
<!-- What should have happened? -->
## ❌ Actual Behavior
<!-- What actually happened? -->
## 🖥️ Environment
- **YTSage Version**: <!-- e.g., v4.5.0 -->
- **yt-dlp Version**: <!-- e.g., 2025.11.12 -->
- **OS**: <!-- e.g., Windows 11, macOS Ventura, Ubuntu 22.04 -->
- **Python Version**: <!-- e.g., 3.13.6 -->
- **Deno Version**: <!-- e.g., 2.5.6 -->
- **FFmpeg Version**: <!-- Run `ffmpeg -version` -->
- **Installation Method**: <!-- - pip - executable - manual -->
> 💡 You can find most of these details by clicking the About button to open the About dialog.
## 📸 Screenshots/Logs
<!--
Attach screenshots (for GUI issues) or terminal logs (for CLI errors).
To collect log files:
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:
-->
## [ERROR] Failed to download: ...
## 🛠️ Possible Fix
<!-- (Optional) Suggest a fix if you have insights -->
## 📌 Additional Context
<!-- Any other details (e.g., frequency of the issue, related PRs) -->
---
**Checklist**
- [ ] I have searched existing issues to avoid duplicates
- [ ] I have provided **the affected YouTube URL**
- [ ] I have included clear steps to reproduce
- [ ] I have attached relevant screenshots/logs
- [ ] I have provided all required environment details
/label ~bug ~needs-triage
+627
View File
@@ -0,0 +1,627 @@
name: Build Linux Release
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
env:
PYTHON_VERSION: '3.13'
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get version
id: get_version
shell: bash
run: |
version="${{ inputs.version || github.event.inputs.version }}"
echo "Extracted version: $version"
echo "VERSION=$version" >> "$GITHUB_OUTPUT"
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: |
venv
~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
shell: bash
run: |
sudo apt-get update -y
# Tools commonly required by cx_Freeze on Linux packaging
sudo apt-get install -y --no-install-recommends \
rpm alien fakeroot patchelf desktop-file-utils file xz-utils zsync
- name: Create virtual environment and install dependencies
shell: bash
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
pip install --no-cache-dir .
pip install --no-cache-dir cx_Freeze
- name: Prepare build variables
shell: bash
run: |
version="${{ steps.get_version.outputs.VERSION }}"
echo "VERSION=$version" >> "$GITHUB_ENV"
arch="$(uname -m)" # x86_64 or aarch64
echo "ARCH=$arch" >> "$GITHUB_ENV"
echo "Prepared build variables for version: $version on $(uname -a)"
- 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
from pathlib import Path
from cx_Freeze import setup, Executable
version = os.environ.get("VERSION", "0.0.0")
# Prepare include_files list
include_files_list = [
("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"),
("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",
"loguru",
"setuptools",
],
zip_include_packages=[
"PySide6",
"shiboken6",
"requests",
"PIL",
"packaging",
],
excludes=[
"PySide6.QtBluetooth",
"PySide6.QtOpenGL",
"PySide6.QtPrintSupport",
"PySide6.QtTest",
"PySide6.QtXml",
"PySide6.QtSql",
"PySide6.QtHelp",
"PySide6.QtQml",
"PySide6.QtQuick",
"PySide6.QtWebEngineCore",
"PIL.ImageDraw",
"PIL.ImageFont",
"numpy",
"scipy",
"wx",
"pandas",
"tkinter",
"yt_dlp",
"unittest",
"test",
"tests",
"pydoc",
"doctest",
"email",
],
include_files=include_files_list,
# Bundle all dependencies - avoid system library references
bin_includes=[],
bin_excludes=[],
# Important: include system libs to avoid external dependencies
replace_paths=[("*", "")],
)
executables = [
Executable(
script="ytsage_entry.py",
target_name="ytsage",
icon="branding/icons/icon.png",
)
]
setup(
name="ytsage",
version=version,
description="YTSage",
options={
"build_exe": build_exe_options,
# AppImage packaging
"bdist_appimage": {
# If ends with .AppImage, use verbatim file name
"target_name": f"ytsage-v{version}.AppImage",
},
},
executables=executables,
)
PY
- name: Create desktop entry
shell: bash
run: |
cat > ytsage.desktop <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=YTSage
Comment=YouTube downloader
Exec=/usr/bin/ytsage %U
Icon=ytsage
Terminal=false
Categories=AudioVideo;Network;Utility;
StartupWMClass=YTSage
DESKTOP
- name: Build AppImage and RPM
shell: bash
run: |
set -e
source venv/bin/activate
# Ensure a clean build and build_exe first with -OO
python -OO setup_cxfreeze.py build_exe
# Clean unnecessary files from build directory before packaging
build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true)
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 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 }}"
arch_uname="${ARCH}"
workspace_root="$(pwd)"
build_dir_abs="${workspace_root}/${build_dir}"
# Create custom RPM spec file that doesn't auto-detect dependencies
mkdir -p build/rpm/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
cat > build/rpm/SPECS/ytsage.spec <<SPEC
%global __requires_exclude_from ^/opt/ytsage/.*$
%global __provides_exclude_from ^/opt/ytsage/.*$
%define _build_id_links none
%undefine _missing_build_ids_terminate_build
Name: ytsage
Version: ${version//-/.}
Release: 1%{?dist}
Summary: YTSage - A modern YouTube downloader
License: MIT
URL: https://github.com/oop7/YTSage
AutoReqProv: no
%description
YTSage is a modern YouTube downloader with a PySide6 interface.
Download videos, extract audio, fetch subtitles, and more.
All dependencies are bundled within the package.
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/opt/ytsage
mkdir -p %{buildroot}/usr/bin
mkdir -p %{buildroot}/usr/share/applications
mkdir -p %{buildroot}/usr/share/pixmaps
# Copy application files
cp -a ${build_dir_abs}/* %{buildroot}/opt/ytsage/
# Create wrapper script
cat > %{buildroot}/usr/bin/ytsage <<'WRAPPER'
#!/usr/bin/env bash
set -euo pipefail
APPDIR="/opt/ytsage"
cd "\$APPDIR"
exec "\$APPDIR/ytsage" "\$@"
WRAPPER
chmod 0755 %{buildroot}/usr/bin/ytsage
# Install desktop file and icon
install -m 0644 ${workspace_root}/ytsage.desktop %{buildroot}/usr/share/applications/ytsage.desktop
install -m 0644 ${workspace_root}/branding/icons/icon.png %{buildroot}/usr/share/pixmaps/ytsage.png
%files
/opt/ytsage
/usr/bin/ytsage
/usr/share/applications/ytsage.desktop
/usr/share/pixmaps/ytsage.png
%post
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
%postun
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
%changelog
* $(date +'%a %b %d %Y') YTSage Maintainers <noreply@example.com> - ${version//-/.}-1
- Release ${version}
SPEC
# Build the RPM
rpmbuild -bb build/rpm/SPECS/ytsage.spec \
--define "_topdir $(pwd)/build/rpm" \
--buildroot "$(pwd)/build/rpm/BUILDROOT"
# Copy RPM to dist
mkdir -p dist
find build/rpm/RPMS -name "*.rpm" -exec cp {} dist/ \;
echo "Post-build directory listing:"
echo "-- dist --"; ls -lah dist || true
echo "-- build --"; ls -lah build || true
- name: Build native DEB package
shell: bash
run: |
set -e
version="${{ steps.get_version.outputs.VERSION }}"
arch_uname="${ARCH}"
# Map uname -m to Debian arch names
case "$arch_uname" in
x86_64) deb_arch=amd64 ;;
aarch64) deb_arch=arm64 ;;
*) deb_arch="$arch_uname" ;;
esac
# Locate build output
build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1)
if [ -z "$build_dir" ]; then
echo "Error: build/exe.* directory not found" >&2
exit 1
fi
# Staging root
pkgroot=deb_pkg
rm -rf "$pkgroot"
mkdir -p "$pkgroot/DEBIAN" \
"$pkgroot/usr/bin" \
"$pkgroot/usr/share/applications" \
"$pkgroot/usr/share/pixmaps" \
"$pkgroot/opt/ytsage"
# Install application payload under /opt/ytsage
cp -a "$build_dir"/* "$pkgroot/opt/ytsage/"
# Wrapper to ensure correct working directory
cat > "$pkgroot/usr/bin/ytsage" <<'WRAP'
#!/usr/bin/env bash
set -euo pipefail
APPDIR="/opt/ytsage"
cd "$APPDIR"
exec "$APPDIR/ytsage" "$@"
WRAP
chmod 0755 "$pkgroot/usr/bin/ytsage"
# Desktop file and icon
install -m 0644 ytsage.desktop "$pkgroot/usr/share/applications/ytsage.desktop"
install -m 0644 branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png"
# Control file
cat > "$pkgroot/DEBIAN/control" <<CONTROL
Package: ytsage
Version: ${version}
Section: utils
Priority: optional
Architecture: ${deb_arch}
Maintainer: YTSage Maintainers <noreply@example.com>
Homepage: https://github.com/oop7/YTSage
Description: YTSage - A modern YouTube downloader with a PySide6 interface
Download videos, extract audio, fetch subtitles, and more.
CONTROL
# Post-install script to refresh desktop/menu caches (best-effort)
cat > "$pkgroot/DEBIAN/postinst" <<'POSTINST'
#!/bin/sh
set -e
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
fi
exit 0
POSTINST
chmod 0755 "$pkgroot/DEBIAN/postinst"
# Triggers so Debian updates caches
cat > "$pkgroot/DEBIAN/triggers" <<'TRIGGERS'
interest-noawait /usr/share/applications
interest-noawait /usr/share/icons/hicolor
TRIGGERS
# Build the deb
deb_out="YTSage-v${version}-${deb_arch}.deb"
dpkg-deb --build "$pkgroot" "$deb_out"
mkdir -p artifacts
mv "$deb_out" artifacts/
echo "Built native DEB: artifacts/$deb_out"
- name: Build Flatpak Bundle
shell: bash
run: |
version="${{ steps.get_version.outputs.VERSION }}"
# Install flatpak-builder
sudo apt-get install -y flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
# Define manifest ID
APP_ID="io.github.oop7.YTSage"
# Create a specific desktop file for Flatpak
# - Exec must be 'ytsage' (in path), not /usr/bin/ytsage
# - Icon must match the App ID (io.github.oop7.YTSage)
cat > flatpak.desktop <<EOF
[Desktop Entry]
Type=Application
Name=YTSage
Comment=YouTube downloader
Exec=ytsage
Icon=$APP_ID
Terminal=false
Categories=AudioVideo;Network;Utility;
StartupWMClass=YTSage
EOF
# Create manifest manually
cat > ytsage_flatpak.json <<EOF
{
"app-id": "$APP_ID",
"runtime": "org.kde.Platform",
"runtime-version": "6.10",
"sdk": "org.kde.Sdk",
"command": "ytsage",
"finish-args": [
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--socket=pulseaudio",
"--device=dri",
"--share=network",
"--filesystem=host",
"--env=YTDLP_APP_BIN_PATH=/var/data/yt-dlp",
"--env=DENO_APP_BIN_PATH=/var/data/deno"
],
"modules": [
{
"name": "ytsage",
"buildsystem": "simple",
"build-commands": [
"mkdir -p /app/bin /app/share/ytsage",
"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 branding/icons/icon.png /app/share/icons/hicolor/128x128/apps/$APP_ID.png"
],
"sources": [
{
"type": "dir",
"path": "."
}
]
}
]
}
EOF
# Build the Flatpak
# Note: In a real environment, building from source is preferred.
# Here we are "bundling" the already built binaries from the previous step (cx_Freeze) for simplicity in CI.
# This requires the 'dist/' folder to be populated by the previous 'Create cx_Freeze setup script' + build steps.
# Wait, the previous steps built into 'build/exe.linux-...' not 'dist/'. Let's find it.
build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1)
if [ -z "$build_dir" ]; then
echo "Error: build/exe.* directory not found for Flatpak build" >&2
exit 1
fi
# Move build dir to a fixed location for the manifest source to catch
mkdir -p dist/ytsage-v${version}-flatpak
cp -r "$build_dir"/* "dist/ytsage-v${version}-flatpak/"
# Install runtime/sdk
flatpak install -y --user flathub org.kde.Platform//6.10 org.kde.Sdk//6.10
# Build
flatpak-builder --user --install-deps-from=flathub --repo=repo --force-clean build-flatpak ytsage_flatpak.json
# Bundle
flatpak build-bundle repo artifacts/YTSage-v${version}-x86_64.flatpak $APP_ID
echo "Built Flatpak: artifacts/YTSage-v${version}-x86_64.flatpak"
- name: Package and prepare release artifacts (.AppImage, .rpm, .deb)
shell: bash
run: |
version="${{ steps.get_version.outputs.VERSION }}"
mkdir -p artifacts
# Copy AppImage
appimage_src=$(ls dist/ytsage-v*.AppImage 2>/dev/null | head -n1 || true)
if [ -n "$appimage_src" ]; then
cp "$appimage_src" "artifacts/YTSage-v${version}-${ARCH}.AppImage"
echo "Copied AppImage: $appimage_src -> artifacts/YTSage-v${version}-${ARCH}.AppImage"
else
echo "Warning: No .AppImage found in dist/"
fi
# Copy RPM (normalize name)
rpm_src=$(ls dist/ytsage-*.rpm 2>/dev/null | head -n1 || true)
if [ -n "$rpm_src" ]; then
cp "$rpm_src" "artifacts/YTSage-v${version}-${ARCH}.rpm"
echo "Copied RPM: $rpm_src -> artifacts/YTSage-v${version}-${ARCH}.rpm"
else
echo "Warning: No .rpm found in dist/"
fi
# DEB is already staged in artifacts by the native packaging step
ls -1 artifacts/*.deb 2>/dev/null || echo "Warning: No .deb found in artifacts/"
# Check for Flatpak
ls -1 artifacts/*.flatpak 2>/dev/null || echo "Warning: No .flatpak found in artifacts/"
echo "Final artifacts:"
ls -lh artifacts || true
- name: Create/Update draft release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.get_version.outputs.VERSION }}
name: YTSage v${{ steps.get_version.outputs.VERSION }}
draft: true
prerelease: false
append_body: true
fail_on_unmatched_files: false
body: |
# YTSage v${{ steps.get_version.outputs.VERSION }}
**Release Date**: ${{ github.event.head_commit.timestamp }}
files: |
artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+322
View File
@@ -0,0 +1,322 @@
name: Build macOS Release
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
env:
PYTHON_VERSION: '3.13'
jobs:
build-macos:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get version
id: get_version
shell: bash
run: |
version="${{ inputs.version || github.event.inputs.version }}"
echo "Extracted version: $version"
echo "VERSION=$version" >> "$GITHUB_OUTPUT"
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: |
venv
~/.cache/pip
~/Library/Caches/pip
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Create virtual environment and install dependencies
shell: bash
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
pip install --no-cache-dir .
pip install --no-cache-dir cx_Freeze dmgbuild
- name: Prepare build variables
shell: bash
run: |
version="${{ steps.get_version.outputs.VERSION }}"
echo "VERSION=$version" >> "$GITHUB_ENV"
arch="$(uname -m)" # arm64 or x86_64
echo "ARCH=$arch" >> "$GITHUB_ENV"
if [ "$arch" = "arm64" ]; then
echo "ARCH_SUFFIX=arm64" >> "$GITHUB_ENV"
else
echo "ARCH_SUFFIX=x64" >> "$GITHUB_ENV"
fi
echo "Prepared build variables for version: $version on $(uname -a)"
- 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
version = os.environ.get("VERSION", "0.0.0")
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",
"loguru",
"setuptools",
],
zip_include_packages=[
"PySide6",
"shiboken6",
"requests",
"PIL",
"packaging",
],
excludes=[
"PySide6.QtBluetooth",
"PySide6.QtOpenGL",
"PySide6.QtPrintSupport",
"PySide6.QtSvg",
"PySide6.QtTest",
"PySide6.QtXml",
"PySide6.QtSql",
"PySide6.QtHelp",
"PySide6.QtQml",
"PySide6.QtQuick",
"PySide6.QtWebEngineCore",
"PIL.ImageDraw",
"PIL.ImageFont",
"numpy",
"scipy",
"wx",
"pandas",
"tkinter",
"yt_dlp",
"unittest",
"test",
"tests",
"pydoc",
"doctest",
"email",
],
include_files=[
("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="ytsage_entry.py",
target_name=f"YTSage-v{version}",
icon="branding/icons/icon.icns",
)
]
setup(
name="YTSage",
version=version,
description="YTSage",
options={
"build_exe": build_exe_options,
"bdist_mac": {
"iconfile": "branding/icons/icon.icns",
"bundle_name": f"YTSage-v{version}",
},
"bdist_dmg": {
"volume_label": f"YTSage v{version}",
"applications_shortcut": True,
"format": "UDZO",
"filesystem": "HFS+",
"default_view": "icon-view",
},
},
executables=executables,
)
PY
- name: Build .app bundle (bdist_mac)
shell: bash
run: |
source venv/bin/activate
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
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" ]; 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
run: |
version="${{ steps.get_version.outputs.VERSION }}"
echo "Preparing artifacts for version: $version"
mkdir -p artifacts
# 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
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" ]; then
app_base="$(basename "$app_path")"
app_parent="$(dirname "$app_path")"
# 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"
# 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"
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 "Error: .app bundle not found in dist/ or build/"
exit 1
fi
echo "Final artifacts:"
ls -lh artifacts || true
- name: Create/Update draft release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.get_version.outputs.VERSION }}
name: YTSage v${{ steps.get_version.outputs.VERSION }}
draft: true
prerelease: false
append_body: true
fail_on_unmatched_files: false
body: |
# YTSage v${{ steps.get_version.outputs.VERSION }}
**Release Date**: ${{ github.event.head_commit.timestamp }}
files: |
artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+52
View File
@@ -0,0 +1,52 @@
name: Build PyPI Package
on:
workflow_dispatch:
inputs:
version:
description: 'Version name for the release (e.g., 1.0.0)'
required: true
type: string
workflow_call:
inputs:
version:
description: 'Version name for the release (e.g., 1.0.0)'
required: true
type: string
permissions:
contents: write
jobs:
build-pypi:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build PyPI package
run: |
python build_release.py
- name: Upload Release Assets
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version || github.event.inputs.version }}
name: YTSage v${{ inputs.version || github.event.inputs.version }}
draft: true
prerelease: false
files: |
dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+499
View File
@@ -0,0 +1,499 @@
name: Build Windows Release
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
env:
PYTHON_VERSION: '3.13'
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get version
id: get_version
shell: powershell
run: |
$version = "${{ inputs.version || github.event.inputs.version }}"
Write-Host "Extracted version: $version"
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: |
venv
~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Create virtual environment and install dependencies
shell: powershell
run: |
python -m venv venv
.\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install --no-cache-dir .
pip install --no-cache-dir cx_Freeze
- name: Prepare build variables
shell: powershell
run: |
$version = "${{ steps.get_version.outputs.VERSION }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
Write-Host "Prepared build variables for version: $version"
- 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"
Add-Content -Path "setup_cxfreeze.py" -Value "from cx_Freeze import setup, Executable"
Add-Content -Path "setup_cxfreeze.py" -Value ""
Add-Content -Path "setup_cxfreeze.py" -Value 'version = os.environ.get("VERSION", "0.0.0")'
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 ' "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.QtOpenGL",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtPrintSupport",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtSvg",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtTest",'
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.QtQml",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtQuick",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PySide6.QtWebEngineCore",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL.ImageDraw",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "PIL.ImageFont",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "numpy",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "scipy",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "wx",'
Add-Content -Path "setup_cxfreeze.py" -Value ' "pandas",'
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 ' ("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="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="branding/icons/YTSage.ico",'
Add-Content -Path "setup_cxfreeze.py" -Value " )"
Add-Content -Path "setup_cxfreeze.py" -Value "]"
Add-Content -Path "setup_cxfreeze.py" -Value ""
Add-Content -Path "setup_cxfreeze.py" -Value "setup("
Add-Content -Path "setup_cxfreeze.py" -Value ' name="YTSage",'
Add-Content -Path "setup_cxfreeze.py" -Value " version=version,"
Add-Content -Path "setup_cxfreeze.py" -Value ' description="YTSage",'
Add-Content -Path "setup_cxfreeze.py" -Value ' options={"build_exe": build_exe_options},'
Add-Content -Path "setup_cxfreeze.py" -Value " executables=executables,"
Add-Content -Path "setup_cxfreeze.py" -Value ")"
- name: Build Standard Version (ZIP)
shell: powershell
run: |
.\venv\Scripts\Activate.ps1
$version = "$env:VERSION"
Write-Host "Building Standard Version v$version ..."
# Clean previous build artifacts
if (Test-Path "build\exe.*") { Remove-Item "build\exe.*" -Recurse -Force }
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 with extra optimization
python -OO setup_cxfreeze.py build_exe --build-exe "dist\YTSage"
- 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
shell: powershell
run: |
Write-Host "Setting up FFmpeg for bundled version..."
# Create ffmpeg directory
New-Item -ItemType Directory -Path "ffmpeg-temp" -Force
# Download FFmpeg
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
Write-Host "Downloading FFmpeg from: $ffmpegUrl"
Invoke-WebRequest -Uri $ffmpegUrl -OutFile "ffmpeg-temp\ffmpeg.zip"
# Extract FFmpeg
Expand-Archive -Path "ffmpeg-temp\ffmpeg.zip" -DestinationPath "ffmpeg-temp" -Force
# Find the extracted directory (GyanD ffmpeg has specific naming)
$ffmpegDir = Get-ChildItem -Path "ffmpeg-temp" -Directory | Where-Object { $_.Name -like "ffmpeg-*" } | Select-Object -First 1
if ($ffmpegDir) {
# Set environment variable for the build
$ffmpegBinPath = Join-Path $ffmpegDir.FullName "bin"
Write-Host "FFmpeg binaries found at: $ffmpegBinPath"
echo "FFMPEG_PATH=$ffmpegBinPath" >> $env:GITHUB_ENV
# Verify files exist (only ffmpeg and ffprobe are needed)
$ffmpegExe = Join-Path $ffmpegBinPath "ffmpeg.exe"
$ffprobeExe = Join-Path $ffmpegBinPath "ffprobe.exe"
if (Test-Path $ffmpegExe) {
Write-Host "[OK] ffmpeg.exe found"
} else {
Write-Host "[ERROR] ffmpeg.exe missing"
}
if (Test-Path $ffprobeExe) {
Write-Host "[OK] ffprobe.exe found"
} else {
Write-Host "[ERROR] ffprobe.exe missing"
}
} else {
Write-Host "Error: Could not find FFmpeg directory after extraction"
exit 1
}
- name: Build FFmpeg Version (ZIP)
shell: powershell
run: |
.\venv\Scripts\Activate.ps1
$version = "$env:VERSION"
Write-Host "Building FFmpeg Version v$version ..."
# Clean previous build artifacts
if (Test-Path "build\exe.*") { Remove-Item "build\exe.*" -Recurse -Force }
if (Test-Path "build\bdist.*") { Remove-Item "build\bdist.*" -Recurse -Force }
# Create modified setup script for FFmpeg version
(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 -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
if ($env:FFMPEG_PATH) {
$destDir = "dist\YTSage-FFmpeg"
$ffmpegExe = Join-Path $env:FFMPEG_PATH "ffmpeg.exe"
$ffprobeExe = Join-Path $env:FFMPEG_PATH "ffprobe.exe"
foreach ($file in @($ffmpegExe, $ffprobeExe)) {
if (Test-Path $file) {
$name = Split-Path $file -Leaf
Copy-Item $file -Destination (Join-Path $destDir $name) -Force
Write-Host "Copied $name into $destDir"
} else {
Write-Host "Warning: FFmpeg binary not found: $file"
}
}
}
- 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: |
$version = "${{ steps.get_version.outputs.VERSION }}"
Write-Host "Preparing ZIP artifacts for version: $version"
New-Item -ItemType Directory -Path "artifacts" -Force
# List dist directories
Write-Host "Files in dist directory:"
if (Test-Path "dist") {
Get-ChildItem -Path "dist" -Recurse | ForEach-Object { Write-Host " $($_.FullName)" }
} else {
Write-Host " No dist directory found!"
exit 1
}
# Create Standard ZIP
$standardDist = Join-Path (Get-Location) "dist\YTSage"
if (Test-Path $standardDist) {
$stdZip = "artifacts\YTSage-v$version-portable.zip"
if (Test-Path $stdZip) { Remove-Item $stdZip -Force }
Compress-Archive -Path "$standardDist\*" -DestinationPath $stdZip -CompressionLevel Optimal
Write-Host "Created: $(Resolve-Path $stdZip)"
} else {
Write-Host "Warning: Standard dist folder not found at $standardDist"
}
# Create FFmpeg ZIP
$ffmpegDist = Join-Path (Get-Location) "dist\YTSage-FFmpeg"
if (Test-Path $ffmpegDist) {
$ffZip = "artifacts\YTSage-v$version-ffmpeg-portable.zip"
if (Test-Path $ffZip) { Remove-Item $ffZip -Force }
Compress-Archive -Path "$ffmpegDist\*" -DestinationPath $ffZip -CompressionLevel Optimal
Write-Host "Created: $(Resolve-Path $ffZip)"
} else {
Write-Host "Warning: FFmpeg dist folder not found at $ffmpegDist"
}
# List all artifacts
Write-Host "Final artifacts:"
if (Test-Path "artifacts") {
$artifactFiles = Get-ChildItem artifacts
if ($artifactFiles) {
$artifactFiles | ForEach-Object { Write-Host " $($_.Name)" }
} else {
Write-Host " No artifacts created!"
}
} else {
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:
tag_name: v${{ steps.get_version.outputs.VERSION }}
name: YTSage v${{ steps.get_version.outputs.VERSION }}
draft: true
prerelease: false
body: |
# YTSage v${{ steps.get_version.outputs.VERSION }}
**Release Date**: ${{ github.event.head_commit.timestamp }}
files: |
artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+37
View File
@@ -0,0 +1,37 @@
name: Create All Releases
on:
workflow_dispatch:
inputs:
version:
description: 'Version name for the release (e.g., 1.0.0)'
required: true
type: string
permissions:
contents: write
jobs:
release-windows:
uses: ./.github/workflows/build-windows.yml
with:
version: ${{ inputs.version }}
secrets: inherit
release-linux:
uses: ./.github/workflows/build-linux.yml
with:
version: ${{ inputs.version }}
secrets: inherit
release-macos:
uses: ./.github/workflows/build-macos.yml
with:
version: ${{ inputs.version }}
secrets: inherit
release-pypi:
uses: ./.github/workflows/build-pypi.yml
with:
version: ${{ inputs.version }}
secrets: inherit
+34
View File
@@ -0,0 +1,34 @@
# Python bytecode
__pycache__/
*.py[cod]
# Local configuration
*.ini
*.key
*.env
# Packaging artifacts
*.egg-info/
build/
dist/
*.deb
# Logs and misc
*.log
# OS and editor-specific
.DS_Store
.vscode/
.idea/
# yt-dlp temporary/partial files
*.part
*.ytdl
# Virtual environments
venv/
.venv/
# Test artifacts
*.test
*.tmp
+17 -13
View File
@@ -1,18 +1,22 @@
MIT License
Copyright (c) 2026 Houmeres
Copyright (c) 2024 oop7 (YTSage)
Copyright (c) 2026 Houmeres (SageTube)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+6
View File
@@ -0,0 +1,6 @@
include README.md
include LICENSE
include pyproject.toml
recursive-include readme-translations *
recursive-include ytsage/assets *
recursive-include ytsage/languages *
+11
View File
@@ -1,2 +1,13 @@
# SageTube
A watch-first YouTube client for the desktop — search, browse channels and playlists, follow subscriptions, and stream videos in an embedded mpv player, with full yt-dlp download capability inherited from [YTSage](https://github.com/oop7/YTSage).
SageTube is a fork of [YTSage](https://github.com/oop7/YTSage) by [oop7](https://github.com/oop7) (MIT). The original YTSage documentation is preserved at [docs/UPSTREAM_README.md](docs/UPSTREAM_README.md).
## Status
Under active development. See the upstream README for the downloader feature set, which remains fully functional.
## License
MIT — see [LICENSE](LICENSE).
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 160" role="img" aria-labelledby="title" fill="none">
<title id="title">YTSage Logo</title>
<defs>
<!-- Match the hero gradient: white to accent red (#dc2626) -->
<linearGradient id="ytGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#dc2626" />
</linearGradient>
</defs>
<!-- Background transparent rectangle for easier clicking (optional) -->
<rect width="700" height="160" fill="transparent" />
<text x="50%" y="50%"
font-family="Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif"
font-size="120" font-weight="900"
text-anchor="middle" dominant-baseline="central"
fill="url(#ytGradient)">YTSage</text>
</svg>

After

Width:  |  Height:  |  Size: 826 B

+64
View File
@@ -0,0 +1,64 @@
import os
import shutil
import re
import subprocess
import sys
def build_release():
print("Starting Production Build...")
# 1. Backup local README
if os.path.exists("README.md"):
shutil.copy2("README.md", "README.md.bak")
print("Backed up README.md")
try:
# 2. Prepare PyPI README
with open("README.md", "r", encoding="utf-8") as f:
content = f.read()
# Base URL for assets
base_url = "https://github.com/oop7/YTSage/raw/main/branding/"
# Function to fix paths matches by regex
def path_fixer(match):
full_match = match.group(0)
# Convert backslashes to forward slashes for URL compatibility
fixed = full_match.replace("\\", "/")
# Prepend the absolute URL
return fixed.replace("branding/", base_url)
# Regex: matches "branding" followed by backslash or slash, then characters until quote, closing paren, or space
# This captures paths like: branding\screenshots\main.png
pattern = r"branding[\\/][^\"')\s]+"
new_content = re.sub(pattern, path_fixer, content)
with open("README.md", "w", encoding="utf-8") as f:
f.write(new_content)
print("Modified README.md for PyPI (Absolute URLs)")
# 3. Clean previous builds
for folder in ["dist", "build", "ytsage.egg-info"]:
if os.path.exists(folder):
shutil.rmtree(folder)
# 4. Run Build
print("Building Wheel...")
subprocess.check_call([sys.executable, "-m", "build"])
except Exception as e:
print(f"Error during build: {e}")
sys.exit(1)
finally:
# 5. Restore README
if os.path.exists("README.md.bak"):
# Force move back, overwriting the modified one
shutil.move("README.md.bak", "README.md")
print("Restored original README.md")
print("Build Complete. Artifacts are in the /dist folder.")
if __name__ == "__main__":
build_release()
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Modern YouTube downloader with a clean PySide6 interface.**
Download videos in any quality, extract audio, fetch subtitles, and more.
### 🌍 README Languages
English: [EN](README.md)
| Arabic: [AR](readme-translations/README.ar.md)
| German: [DE](readme-translations/README.de.md)
| Spanish: [ES](readme-translations/README.es.md)
| French: [FR](readme-translations/README.fr.md)
| Hindi: [HI](readme-translations/README.hi.md)
| Indonesian: [ID](readme-translations/README.id.md)
| Italian: [IT](readme-translations/README.it.md)
| Japanese: [JA](readme-translations/README.ja.md)
| Polish: [PL](readme-translations/README.pl.md)
| Portuguese: [PT](readme-translations/README.pt.md)
| Russian: [RU](readme-translations/README.ru.md)
| Turkish: [TR](readme-translations/README.tr.md)
| Chinese: [ZH](readme-translations/README.zh.md)
<p align="center">
<a href="#installation">Installation</a> •
<a href="#features">Features</a> •
<a href="#usage">Usage</a> •
<a href="#screenshots">Screenshots</a> •
<a href="#troubleshooting">Troubleshooting</a> •
<a href="#sponsor">Sponsor</a> •
<a href="#contributing">Contributing</a>
</p>
</div>
---
<a id="why-ytsage"></a>
## ❓ Why YTSage?
YTSage is designed for users who want a **simple yet powerful YouTube downloader**. Unlike other tools, it offers:
- A modern and clean PySide6 interface
- One-click downloads for video, audio, and subtitles
- Advanced features like SponsorBlock, subtitle merging, and playlist selection
- Optional Generic Mode for sites supported by yt-dlp beyond YouTube
- Cross-platform support and easy installation
<a id="features"></a>
## ✨ Features
<div align="center">
| Core Features | Advanced Features | Extra Features |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Format Table | 🚫 SponsorBlock Integration | 🎞️ FPS/HDR Display |
| 🎵 Audio Extraction | 📝 Subtitle Selection & Merging | 🔄 Auto Update yt-dlp |
| ✨ Simple UI | 💾 Save Description & Thumbnail | 🛠️ FFmpeg/yt-dlp/Deno Detection |
| 📋 Playlist Support & Selector | 🚀 Speed Limiter | ⚙️ Custom Commands |
| 📑 Chapter Integration | ✂️ Video Section Trimming | 🍪 Login with Cookies |
| 📜 Download History | 🔄 Version Channel Selection | 🌐 Proxy Support |
| 🎚️ Audio Format Conversion | 🎬 Video Format Settings | 🆙 Built-in Updater Tab |
| 🌍 Generic Mode | 🔊 Audio Normalization (EBU R128) | 🌍 Localized in 14 Languages |
| 💾 Playlist Export | ⚙️ Default Quality & Subtitles | |
</div>
<a id="installation"></a>
## 🚀 Installation
### ⚡ Quick Install (Recommended)
Install YTSage via PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Update existing installation</summary>
```bash
pip install --upgrade ytsage
```
</details>
Then launch the application:
```bash
ytsage
```
### 📦 Pre-built Executables
> [👉 Download Latest Release](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Format | Description |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Standard Installer |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | With FFmpeg Included |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable version, no installation needed |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable with FFmpeg, zipped |
<details>
<summary>🛠️ Installation Steps</summary>
1. **EXE Installer (`.exe`)**: Double-click the file and follow the setup wizard.
2. **Portable Version (`.zip`)**: Extract the archive to your desired location and launch `ytsage.exe`.
3. **FFmpeg Included**: Choose versions with FFmpeg included if you don't have FFmpeg installed on your system.
</details>
#### 🐧 Linux
| Format | Description |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian Package |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, Portable |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM Package |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle |
<details>
<summary>🛠️ Installation Steps</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Fix missing dependencies if needed
```
- **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
| Format | Description |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Zipped Application for Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Disk Image Installer for Apple Silicon |
<details>
<summary>🛠️ Installation Steps</summary>
- **DMG Installer (`.dmg`)**: Double-click to mount, then drag `YTSage.app` to your Applications folder.
- **Application Archive (`.zip`)**: Extract the zip and move `YTSage.app` to your Applications folder.
*Note: If you encounter an "Application is damaged" error, see the macOS troubleshooting section below.*
</details>
---
<details>
<summary>💻 Manual Source Installation</summary>
### 1. Clone the repository
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Install dependencies
#### ⚡ Using uv
```bash
uv pip install .
```
#### 📦 Or using standard pip
```bash
pip install .
```
### 3. Run the application
```bash
python -m ytsage.main
```
</details>
<a id="screenshots"></a>
## 📸 Screenshots
<div align="center">
<table>
<tr>
<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="branding/screenshots/audio_format.png" alt="Audio Format Selection" 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>
<td align="center"><em>Custom Options</em></td>
</tr>
</table>
</div>
<a id="usage"></a>
## 📖 Usage
<details>
<summary>🎯 Basic Usage</summary>
1. **Launch YTSage**
2. **Paste YouTube URL** (or use "Paste URL" button)
3. **Click "Analyze"**
4. **Select Format:**
- `Video` for video downloads
- `Audio Only` for audio extraction
5. **Choose Options:**
- Enable Subtitles and select language
- Enable Subtitle Merging
- Save Thumbnail
- Remove Sponsored Segments
- Save Description
- Embed Chapters
6. **Select Output Directory**
7. **Click "Download"**
> 💡 Default download directory is the user's "Downloads" folder.
</details>
<details>
<summary>📋 Playlist Download</summary>
1. **Paste Playlist URL**
2. **Click "Analyze"**
3. **Select videos from the playlist selector (optional, defaults to all)**
4. **Choose desired format/quality**
5. **Click "Download"**
> 💡 The application automatically handles the download queue, and you can export playlist entries as `.txt`, `.csv`, `.m3u`, or `.json`.
</details>
<details>
<summary>🌍 Generic Mode for Non-YouTube Sites</summary>
Use Generic Mode when you want YTSage to accept URLs from sites supported by yt-dlp, such as Dailymotion, CBC Gem, TikTok, and others.
How to use it:
1. Open `Download Settings`.
2. Toggle on `Generic Mode`.
3. Paste a supported video or playlist URL that is not from YouTube.
4. Click `Analyze`.
5. Choose a format and download as usual.
Notes:
- Generic mode only changes the URL validation inside YTSage. The target site must still be supported by your installed version of yt-dlp.
- Some sites require cookies, login sessions, proxy, or extra yt-dlp arguments depending on the extractor.
- If a site fails, update yt-dlp from the built-in updater tab first before reporting an issue.
</details>
<details>
<summary>🧰 Media & Download Options</summary>
- **Subtitle Options:** Filter languages and embed subtitles into the video file.
- **Subtitle Merging:** Merge subtitles into the video file for hardcoded/burned-in subtitles.
- **Save Description:** Save the video description as a text file.
- **Save Thumbnail:** Save the video thumbnail as an image file.
- **Embed Chapters:** Embed chapter markers as metadata for compatible video players.
- **Remove Sponsored Segments:** Remove sponsored segments from the video using SponsorBlock.
- **Trim Video:** Download only specific parts of a video by specifying time ranges in `HH:MM:SS` format.
</details>
<details>
<summary>⚙️ Output & File Settings</summary>
- **Speed Limiter:** Limit download speed, e.g., `500K` for 500 KB/s.
- **Save Download Path:** Saves the default download path for future downloads. Available in **Download Settings → Download Path**.
- **Default Video Resolution:** Set your preferred default video resolution for auto-selection (e.g., 1080p, 720p). Available in **Download Settings → Default Video Resolution**.
- **Default Subtitle Languages:** Set default subtitle languages for auto-selection (comma-separated, e.g., `en,es`). Available in **Download Settings → Default Subtitle Languages**.
- **Output Filename Format:** Customize the output filename format using variables like `%(title)s`, `%(uploader)s`, `%(playlist_index)s`, and `%(resolution)s`. Available in **Download Settings → Filename Format**.
- **Force Output Format:** Force video downloads into a specific container format like `mp4`, `webm`, or `mkv`. Available in **Download Settings → Output Format Settings**.
- **Audio Format Conversion:** Convert audio-only downloads into preferred formats such as `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, or `Best`. Available in **Download Settings → Audio Format Settings**.
- **Audio Normalization:** Standardize volume for audio-only downloads using EBU R128.
- **Concurrent Connections:** Dramatically increase download speed by downloading files in multiple fragments simultaneously. Available in **Download Settings → General → Concurrent Connections** (Default is 1, maximum recommended is 8-10 to avoid IP throttling).
</details>
<details>
<summary>🌐 Access & Network</summary>
- **Login with Cookies:** Log in to YouTube using cookies to access private content.
How to use it:
1. **Recommended:** Use the built-in `Extract cookies from browser` option in the app, then select your browser and optionally a profile.
2. Alternatively, extract cookies manually:
a. Export browser cookies using an extension like [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Copy cookies in Netscape format
c. Create a file named `cookies.txt` and paste cookies
d. Select the `cookies.txt` file in the app
- **Proxy Support:** Use a proxy server for downloads, e.g., `http://<proxy-server>:<port>`
- **Generic Mode:** Allows YTSage to analyze and download from non-YouTube sites supported by yt-dlp. Enable from **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Tools & Maintenance</summary>
- **Custom Commands:** Access advanced yt-dlp features via command-line arguments.
- **Updater Tab:** Manage built-in update tools from one place in Custom Options:
- **yt-dlp Updates:** Check for updates and toggle between Stable and Nightly release channels.
- **FFmpeg Version Checker:** Check your FFmpeg version and open installation guides.
- **Deno Updates:** Check and update the Deno runtime.
- **FFmpeg/yt-dlp/Deno Detection:** Automatically detects paths and versions for FFmpeg, yt-dlp, and Deno from the About dialog.
- **Download History:** View past downloads with thumbnails and statuses from the **History** button.
</details>
<details>
<summary>🌍 Localization</summary>
YTSage supports **14 languages** for global accessibility. Select your preferred language in **Custom Options → Language**.
### Supported Languages
| Language | Code | Language | Code |
|----------|------|----------|------|
| 🇺🇸 English | `en` | 🇪🇸 Spanish | `es` |
| 🇸🇦 Arabic | `ar` | 🇫🇷 French | `fr` |
| 🇩🇪 German | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonesian | `id` | 🇮🇹 Italian | `it` |
| 🇯🇵 Japanese | `ja` | 🇵🇱 Polish | `pl` |
| 🇧🇷 Portuguese | `pt` | 🇷🇺 Russian | `ru` |
| 🇹🇷 Turkish | `tr` | 🇨🇳 Chinese | `zh` |
### README Translations
| Language | File | Language | File |
|----------|------|----------|------|
| 🇺🇸 English | [README.md](README.md) | 🇪🇸 Spanish | [readme-translations/README.es.md](readme-translations/README.es.md) |
| 🇸🇦 Arabic | [readme-translations/README.ar.md](readme-translations/README.ar.md) | 🇫🇷 French | [readme-translations/README.fr.md](readme-translations/README.fr.md) |
| 🇩🇪 German | [readme-translations/README.de.md](readme-translations/README.de.md) | 🇮🇳 Hindi | [readme-translations/README.hi.md](readme-translations/README.hi.md) |
| 🇮🇩 Indonesian | [readme-translations/README.id.md](readme-translations/README.id.md) | 🇮🇹 Italian | [readme-translations/README.it.md](readme-translations/README.it.md) |
| 🇯🇵 Japanese | [readme-translations/README.ja.md](readme-translations/README.ja.md) | 🇵🇱 Polish | [readme-translations/README.pl.md](readme-translations/README.pl.md) |
| 🇧🇷 Portuguese | [readme-translations/README.pt.md](readme-translations/README.pt.md) | 🇷🇺 Russian | [readme-translations/README.ru.md](readme-translations/README.ru.md) |
| 🇹🇷 Turkish | [readme-translations/README.tr.md](readme-translations/README.tr.md) | 🇨🇳 Chinese | [readme-translations/README.zh.md](readme-translations/README.zh.md) |
> 💡 **Want to contribute a translation?** Check out the [Contributing](#contributing) section to help us add more languages!
</details>
<a id="troubleshooting"></a>
## 🛠️ Troubleshooting
<details>
<summary>Click to view common issues and solutions</summary>
- **Format table not appearing:** Update yt-dlp to latest version and switch to nightly yt-dlp.
- **Download failed:** Check your internet connection and ensure the video is available.
- **Specific Download Errors:**
- **Private Videos:** Use cookie authentication to access private content.
- **Age-Restricted Content:** Log in to your YouTube account to view age-restricted videos.
- **Geo-Blocked Videos:** Consider using a VPN to bypass regional restrictions.
- **Deleted Videos:** Video is no longer available on YouTube.
- **Live Streams:** Live streams cannot be downloaded; wait for the broadcast to end.
- **Network Errors:** Check your internet connection and try again.
- **Invalid URLs:** Ensure the URL is correct and from a supported platform.
- **Premium Content:** Requires a YouTube Premium subscription.
- **Copyright Blocks:** Content is blocked due to copyright restrictions.
- **Video and Audio Files separate after download:** This happens when FFmpeg is missing or not detected. YTSage requires FFmpeg to merge high-quality video and audio streams.
- **Solution:** Ensure FFmpeg is installed and accessible in your system's PATH. For Windows users, the easiest option is to download the `YTSage-v<version>-ffmpeg.exe` file, which comes bundled with FFmpeg.
---
#### 🛡️ Windows Defender / Antivirus Warning
Some antivirus software may flag `.exe` files as false positives. This is a **known limitation** of packaged applications.
**Why this happens:**
- Antivirus heuristics can mistakenly identify packaged executables as suspicious.
**Safe Alternatives:**
- ✅ **Use pip install:** `pip install ytsage` (Recommended)
- ✅ **Build from Source**: by following this [guide](.github/CI_CD_README.md)
- ✅ **Whitelist the app** in your antivirus software.
#### 🍎 macOS: "Application is damaged and cannot be opened"
If you see this error on macOS Sonoma or newer, you need to remove the quarantine attribute.
1. **Open Terminal** (you can find this using Spotlight).
2. **Type the following command** but **do not** press Enter yet. Make sure to include the space at the end:
```bash
xattr -d com.apple.quarantine
```
3. **Drag the `YTSage.app` file** from your Finder window and drop it directly into the Terminal window. This will automatically paste the correct file path.
4. **Press Enter** to run the command.
5. **Try opening YTSage.app again.** It should now launch correctly.
---
#### **Config Locations (Advanced)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Sponsor
If YTSage saves you time, please consider sponsoring the project. Sponsoring helps cover development time, testing across all platforms, and future improvements.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Sponsorship link is also available directly in the app via the About dialog.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contributing"></a>
## 👥 Contributing
We welcome contributions! Heres how you can help:
1. 🍴 Fork the repository
2. 🌿 Create your feature branch:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 Commit your changes:
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 Push to the branch:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 Open a Pull Request
### 🌍 Contributing Translations
- Update the relevant localized README file (e.g., `readme-translations/README.fr.md`)
- Keep app strings synced by editing `ytsage/languages/<code>.json`
- If your language is missing, start from `README.md` and create `readme-translations/README.<code>.md`
<details>
<summary>📂 Project Structure</summary>
## YTSage - Project Structure
This document describes the organized folder structure of YTSage.
### 📁 Project Structure
```
YTSage/
├── 📁 .github/ # GitHub configuration
│ ├── 📁 ISSUE_TEMPLATE/ # Issue templates
│ │ └── 🐛-bug-report.md # Bug report template
│ ├─── 📁 workflows/ # GitHub Actions workflows
│ │ ├── build-linux.yml # Linux build workflow
│ │ ├── build-macos.yml # macOS build workflow
│ │ │── build-windows.yml # Windows build workflow
| | └── release-all.yml # Release master workflow
│ └── 📄 CI_CD_README.md # CI/CD documentation
├── 📁 branding/ # Branding assets (Screenshots, SVGs)
│ ├── 📁 icons/ # App icons
│ ├── 📁 screenshots/ # Documentation screenshots
│ └── 📁 svg/ # SVG assets
├── 📄 LICENSE # License file
├── 📄 pyproject.toml # Project metadata and dependencies
├── 📄 README.md # Project documentation
├── 📄 requirements.txt # Python dependencies (dev)
└── 📁 ytsage/ # Source package
├── 📁 assets/ # Runtime assets
│ ├── 📁 Icon/ # App icons
│ └── 📁 sound/ # Sound 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
│ ├── 📄 ytsage_downloader.py # Download functionality
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg integration
│ ├── 📄 ytsage_utils.py # Utility functions
│ └── 📄 ytsage_yt_dlp.py # yt-dlp integration
├── 📁 gui/ # UI components
│ ├── 📄 __init__.py # GUI package init
│ ├── 📄 ytsage_gui_main.py # Main app window
│ └── 📁 ytsage_gui_dialogs/ # Dialog classes
├── 📁 utils/ # Utility modules
│ ├── 📄 __init__.py # Utils package init
│ ├── 📄 ytsage_config_manager.py # Config management
│ └── 📄 ytsage_logger.py # Logging utilities
├── 📄 __init__.py # Package entry point
└── 📄 main.py # Main execution script
```
</details>
## ⭐️ Star History
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
<details>
<summary>Show Acknowledgments</summary>
<div align="center">
<p>A big thanks to everyone who contributed to this project by opening an issue to suggest an improvement or report a bug.</p>
<table>
<tr class="section"><th colspan="2">Core Components</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Download Engine</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Media Processing</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime for yt-dlp plugins</td>
</tr>
<tr class="section"><th colspan="2">Libraries & Frameworks</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI Framework</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Image Processing</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>HTTP Requests</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Version/Package Management</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown Rendering</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td>
</tr>
<tr class="section"><th colspan="2">Assets & Contributors</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>Notification Sound</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Code Contributor</td>
</tr>
</table>
</div>
</details>
## ⚠️ Disclaimer
This tool is for personal use only. Please respect YouTube's Terms of Service and content creator rights.
---
<div align="center">
Made with ❤️ by [oop7](https://github.com/oop7)
</div>
+68
View File
@@ -0,0 +1,68 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "ytsage"
version = "5.2.0"
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",
]
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**برنامج تحميل من يوتيوب عصري بواجهة PySide6 أنيقة.**
قم بتحميل الفيديوهات بأي جودة، استخراج الصوت، جلب الترجمات، والمزيد.
### 🌍 لغات README
الإنجليزية: [EN](../README.md)
| العربية: [AR](README.ar.md)
| الألمانية: [DE](README.de.md)
| الإسبانية: [ES](README.es.md)
| الفرنسية: [FR](README.fr.md)
| الهندية: [HI](README.hi.md)
| الإندونيسية: [ID](README.id.md)
| الإيطالية: [IT](README.it.md)
| اليابانية: [JA](README.ja.md)
| البولندية: [PL](README.pl.md)
| البرتغالية: [PT](README.pt.md)
| الروسية: [RU](README.ru.md)
| التركية: [TR](README.tr.md)
| الصينية: [ZH](README.zh.md)
<p align="center">
<a href="#installation">التثبيت</a> •
<a href="#features">المميزات</a> •
<a href="#usage">الاستخدام</a> •
<a href="#screenshots">لقطات الشاشة</a> •
<a href="#troubleshooting">استكشاف الأخطاء</a> •
<a href="#sponsor">الدعم</a> •
<a href="#contributing">المساهمة</a>
</p>
</div>
---
<a id="why-ytsage"></a>
## ❓ لماذا YTSage؟
تم تصميم YTSage للمستخدمين الذين يريدون **أداة تحميل يوتيوب بسيطة لكن قوية**. على عكس الأدوات الأخرى، فإنه يوفر:
- واجهة PySide6 عصرية ونظيفة
- تحميل بنقرة واحدة للفيديو والصوت والترجمات
- ميزات متقدمة مثل SponsorBlock، دمج الترجمات، واختيار قوائم التشغيل
- وضع عام (Generic Mode) اختياري للمواقع التي يدعمها yt-dlp بخلاف يوتيوب
- دعم منصات متعددة وتثبيت سهل
<a id="features"></a>
## ✨ المميزات
<div align="center">
| الميزات الأساسية | الميزات المتقدمة | ميزات إضافية |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 جدول الصيغ | 🚫 تكامل SponsorBlock | 🎞️ عرض FPS/HDR |
| 🎵 استخراج الصوت | 📝 اختيار ودمج الترجمات | 🔄 تحديث تلقائي لـ yt-dlp |
| ✨ واجهة مستخدم بسيطة | 💾 حفظ الوصف والصورة المصغرة | 🛠️ اكتشاف FFmpeg/yt-dlp/Deno |
| 📋 دعم ومحدد قوائم التشغيل | 🚀 محدد السرعة | ⚙️ أوامر مخصصة |
| 📑 دمج الفصول | ✂️ قص أجزاء الفيديو | 🍪 تسجيل الدخول بالكوكيز |
| 📜 سجل التحميلات | 🔄 اختيار قناة الإصدار | 🌐 دعم البروكسي |
| 🎚️ تحويل صيغ الصوت | 🎬 إعدادات صيغ الفيديو | 🆙 تبويب تحديث مدمج |
| 🌍 الوضع العام | 🔊 تطبيع الصوت (EBU R128) | 🌍 دعم 14 لغة |
| 💾 تصدير قوائم التشغيل | ⚙️ الجودة والترجمات الافتراضية | |
</div>
<a id="installation"></a>
## 🚀 التثبيت
### ⚡ التثبيت السريع (موصى به)
تثبيت YTSage عبر PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 تحديث نسخة مثبتة</summary>
```bash
pip install --upgrade ytsage
```
</details>
ثم قم بتشغيل التطبيق:
```bash
ytsage
```
### 📦 حزم جاهزة للتشغيل
> [👉 تحميل أحدث إصدار](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| الصيغة | الوصف |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | مثبت قياسي |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | مع FFmpeg مدمج |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | نسخة محمولة، لا تحتاج لتثبيت |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | محمولة مع FFmpeg، مضغوطة |
<details>
<summary>🛠️ خطوات التثبيت</summary>
1. **مثبت EXE (`.exe`)**: انقر نقرًا مزدوجًا على الملف واتبع معالج التثبيت.
2. **النسخة المحمولة (`.zip`)**: استخرج الملف في أي مكان وشغل `ytsage.exe`.
3. **FFmpeg المدمج**: اختر النسخة التي تحتوي على FFmpeg إذا لم يكن مثبتًا على نظامك.
</details>
#### 🐧 Linux
| الصيغة | الوصف |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | حزمة Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage، محمولة |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | حزمة RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle |
<details>
<summary>🛠️ خطوات التثبيت</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # لإصلاح الاعتمادات الناقصة إذا لزم الأمر
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: اتبع التعليمات على Flathub أو شغل:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| الصيغة | الوصف |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | تطبيق مضغوط لـ Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | صورة قرص لـ Apple Silicon |
<details>
<summary>🛠️ خطوات التثبيت</summary>
- **مثبت DMG (`.dmg`)**: انقر نقرًا مزدوجًا لفتح القرص، ثم اسحب `YTSage.app` إلى مجلد Applications.
- **تطبيق مضغوط (`.zip`)**: استخرج الملف وانقل `YTSage.app` إلى مجلد Applications.
*ملاحظة: إذا واجهت خطأ "التطبيق تالف"، راجع قسم استكشاف الأخطاء لنظام macOS أدناه.*
</details>
---
<details>
<summary>💻 التثبيت اليدوي من المصدر</summary>
### 1. نسخ المستودع
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. تثبيت الاعتمادات
#### ⚡ باستخدام uv
```bash
uv pip install .
```
#### 📦 أو باستخدام pip العادي
```bash
pip install .
```
### 3. تشغيل التطبيق
```bash
python -m ytsage.main
```
</details>
<a id="screenshots"></a>
## 📸 لقطات الشاشة
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="إعدادات التحميل" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="تحميل قوائم التشغيل" width="400"/></td>
</tr>
<tr>
<td align="center"><em>إعدادات التحميل</em></td>
<td align="center"><em>تحميل قوائم التشغيل</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="اختيار صيغة الصوت" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="خيارات مخصصة" width="400"/></td>
</tr>
<tr>
<td align="center"><em>صيغة الصوت</em></td>
<td align="center"><em>خيارات مخصصة</em></td>
</tr>
</table>
</div>
<a id="usage"></a>
## 📖 الاستخدام
<details>
<summary>🎯 الاستخدام الأساسي</summary>
1. **شغل YTSage**
2. **الصق رابط يوتيوب** (أو استخدم زر "Paste URL")
3. **اضغط على "Analyze"**
4. **اختر الصيغة:**
- `Video` للتحميلات المرئية
- `Audio Only` لاستخراج الصوت فقط
5. **اختر الخيارات:**
- تفعيل الترجمات واختيار اللغة
- تفعيل دمج الترجمات
- حفظ الصورة المصغرة
- حذف المقاطع الإعلانية (Sponsor segments)
- حفظ الوصف
- دمج الفصول (Chapters)
6. **اختر مجلد الحفظ**
7. **اضغط على "Download"**
> 💡 مجلد التحميل الافتراضي هو مجلد "Downloads" للمستخدم.
</details>
<details>
<summary>📋 تحميل قائمة تشغيل (Playlist)</summary>
1. **الصق رابط قائمة التشغيل**
2. **اضغط على "Analyze"**
3. **اختر الفيديوهات من محدد القائمة (اختياري، يتم اختيار الكل تلقائيًا)**
4. **اختر الصيغة/الجودة المطلوبة**
5. **اضغط على "Download"**
> 💡 التطبيق يتعامل مع طابور التحميل تلقائيًا، ويمكنك تصدير مدخلات القائمة كملفات `.txt` أو `.csv` أو `.m3u` أو `.json`.
</details>
<details>
<summary>🌍 الوضع العام للمواقع غير يوتيوب</summary>
استخدم الوضع العام (Generic Mode) عندما تريد أن يقبل YTSage روابط من المواقع التي يدعمها yt-dlp، مثل Dailymotion و CBC Gem و TikTok وغيرها.
كيفية الاستخدام:
1. افتح `Download Settings`.
2. فعل `Generic Mode`.
3. الصق رابط فيديو أو قائمة تشغيل مدعومة غير يوتيوب.
4. اضغط على `Analyze`.
5. اختر الصيغة وحمل كالمعتاد.
ملاحظات:
- الوضع العام يغير فقط التحقق من الرابط داخل YTSage. يجب أن يكون الموقع مدعومًا من نسخة yt-dlp المثبتة لديك.
- بعض المواقع تتطلب كوكيز، تسجيل دخول، بروكسي أو وسائط yt-dlp إضافية حسب نوع الموقع.
- إذا فشل موقع ما، حدث yt-dlp أولاً من تبويب التحديث المدمج قبل الإبلاغ عن المشكلة.
</details>
<details>
<summary>🧰 خيارات الميديا والتحميل</summary>
- **خيارات الترجمة:** تصفية اللغات وتضمين الترجمات في ملف الفيديو.
- **دمج الترجمات:** دمج الترجمات داخل ملف الفيديو لتكون ترجمة مدمجة (Hardcoded).
- **حفظ الوصف:** حفظ وصف الفيديو كملف نصي.
- **حفظ الصورة المصغرة:** حفظ صورة الفيديو كملف صورة.
- **دمج الفصول:** تضمين علامات الفصول كبيانات وصفية لمشغلات الفيديو المتوافقة.
- **حذف مقاطع الرعاة:** حذف الأجزاء الإعلانية من الفيديو باستخدام SponsorBlock.
- **قص الفيديو:** تحميل أجزاء محددة فقط من الفيديو عن طريق تحديد أوقات البداية والنهاية بصيغة `HH:MM:SS`.
</details>
<details>
<summary>⚙️ إعدادات الإخراج والملفات</summary>
- **محدد السرعة:** تحديد سرعة التحميل، مثلاً `500K` لـ 500 كيلوبايت/ثانية.
- **حفظ مسار التحميل:** يحفظ مسار التحميل الافتراضي للتحميلات المستقبلية. متاح في **Download Settings → Download Path**.
- **دقة الفيديو الافتراضية:** حدد دقة الفيديو المفضلة للاختيار التلقائي (مثل 1080p أو 720p). متاح في **Download Settings → Default Video Resolution**.
- **لغات الترجمة الافتراضية:** حدد لغات الترجمة للاختيار التلقائي (مفصولة بفواصل، مثل `ar,en`). متاح في **Download Settings → Default Subtitle Languages**.
- **صيغة اسم الملف:** تخصيص صيغة اسم الملف باستخدام متغيرات مثل `%(title)s` و `%(uploader)s` و `%(playlist_index)s` و `%(resolution)s`. متاح في **Download Settings → Filename Format**.
- **فرض صيغة الإخراج:** فرض تحميل الفيديو بصيغة محددة مثل `mp4` أو `webm` أو `mkv`. متاح في **Download Settings → Output Format Settings**.
- **تحويل صيغ الصوت:** تحويل تحميلات الصوت فقط للصيغ المفضلة مثل `AAC` أو `MP3` أو `FLAC` أو `WAV` أو `Opus` أو `M4A` أو `Vorbis` أو `Best`. متاح في **Download Settings → Audio Format Settings**.
- **تطبيع الصوت:** توحيد مستوى الصوت لتحميلات الصوت فقط باستخدام EBU R128.
- **الاتصالات المتزامنة:** زيادة سرعة التحميل بشكل كبير عن طريق تحميل الملف في أجزاء متعددة في وقت واحد. متاح في **Download Settings → General → Concurrent Connections** (القيمة الافتراضية 1، الموصى به 8-10 لتجنب حظر IP).
</details>
<details>
<summary>🌐 الوصول والشبكة</summary>
- **تسجيل الدخول بالكوكيز:** سجل الدخول ليوتيوب باستخدام الكوكيز للوصول للمحتوى الخاص.
كيفية الاستخدام:
1. **موصى به:** استخدم خيار `Extract cookies from browser` المدمج في التطبيق، ثم اختر المتصفح والملف الشخصي.
2. بدلاً من ذلك، استخرج الكوكيز يدويًا:
أ. صدر الكوكيز من متصفحك باستخدام إضافة مثل [cookie-editor](https://github.com/moustachauve/cookie-editor)
ب. انسخ الكوكيز بصيغة Netscape
ج. أنشئ ملفًا باسم `cookies.txt` والصق الكوكيز فيه
د. اختر ملف `cookies.txt` في التطبيق
- **دعم البروكسي:** استخدم خادم بروكسي للتحميلات، مثلاً `http://<proxy-server>:<port>`
- **الوضع العام:** يسمح لـ YTSage بتحليل والتحميل من المواقع غير يوتيوب التي يدعمها yt-dlp. فعله من **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ الأدوات والصيانة</summary>
- **أوامر مخصصة:** الوصول لميزات yt-dlp المتقدمة عبر وسائط موجه الأوامر.
- **تبويب التحديث:** إدارة أدوات التحديث من مكان واحد في الخيارات المخصصة:
- **تحديثات yt-dlp:** التحقق من التحديثات والتبديل بين القناة المستقرة (Stable) والليلية (Nightly).
- **فاحص نسخة FFmpeg:** التحقق من نسخة FFmpeg وعرض أدلة التثبيت.
- **تحديثات Deno:** التحقق وتحديث محرك Deno.
- **اكتشاف FFmpeg/yt-dlp/Deno:** يكتشف تلقائيًا المسارات والنسخ لـ FFmpeg و yt-dlp و Deno من نافذة "About".
- **سجل التحميلات:** عرض التحميلات السابقة مع الصور المصغرة والحالة من زر **History**.
</details>
<details>
<summary>🌍 الترجمة</summary>
يدعم YTSage **14 لغة** للوصول العالمي. اختر لغتك المفضلة من **Custom Options → Language**.
### اللغات المدعومة
| اللغة | الرمز | اللغة | الرمز |
|----------|------|----------|------|
| 🇺🇸 الإنجليزية | `en` | 🇪🇸 الإسبانية | `es` |
| 🇸🇦 العربية | `ar` | 🇫🇷 الفرنسية | `fr` |
| 🇩🇪 الألمانية | `de` | 🇮🇳 الهندية | `hi` |
| 🇮🇩 الإندونيسية | `id` | 🇮🇹 الإيطالية | `it` |
| 🇯🇵 اليابانية | `ja` | 🇵🇱 البولندية | `pl` |
| 🇧🇷 البرتغالية | `pt` | 🇷🇺 الروسية | `ru` |
| 🇹🇷 التركية | `tr` | 🇨🇳 الصينية | `zh` |
### تراجم README
| اللغة | الملف | اللغة | الملف |
|----------|------|----------|------|
| 🇺🇸 الإنجليزية | [README.md](../README.md) | 🇪🇸 الإسبانية | [README.es.md](README.es.md) |
| 🇸🇦 العربية | [README.ar.md](README.ar.md) | 🇫🇷 الفرنسية | [README.fr.md](README.fr.md) |
| 🇩🇪 الألمانية | [README.de.md](README.de.md) | 🇮🇳 الهندية | [README.hi.md](README.hi.md) |
| 🇮🇩 الإندونيسية | [README.id.md](README.id.md) | 🇮🇹 الإيطالية | [README.it.md](README.it.md) |
| 🇯🇵 اليابانية | [README.ja.md](README.ja.md) | 🇵🇱 البولندية | [README.pl.md](README.pl.md) |
| 🇧🇷 البرتغالية | [README.pt.md](README.pt.md) | 🇷🇺 الروسية | [README.ru.md](README.ru.md) |
| 🇹🇷 التركية | [README.tr.md](README.tr.md) | 🇨🇳 الصينية | [README.zh.md](README.zh.md) |
> 💡 **هل تريد المساعدة في الترجمة؟** راجع قسم [المساهمة](#contributing) لمساعدتنا في إضافة المزيد من اللغات!
</details>
<a id="troubleshooting"></a>
## 🛠️ استكشاف الأخطاء
<details>
<summary>اضغط لعرض المشاكل الشائعة وحلولها</summary>
- **جدول الصيغ لا يظهر:** حدث yt-dlp لأحدث نسخة وانتقل للقناة الليلية (Nightly).
- **فشل التحميل:** تحقق من اتصال الإنترنت وتأكد أن الفيديو متاح.
- **أخطاء تحميل محددة:**
- **فيديوهات خاصة:** استخدم الكوكيز للوصول للمحتوى الخاص.
- **محتوى مقيد بالعمر:** سجل دخولك ليوتيوب لمشاهدة الفيديوهات المقيدة.
- **فيديوهات محظورة جغرافيًا:** استخدم VPN لتجاوز القيود الإقليمية.
- **فيديوهات محذوفة:** الفيديو لم يعد متاحًا على يوتيوب.
- **البث المباشر (Live):** لا يمكن تحميل البث المباشر أثناء عرضه؛ انتظر حتى ينتهي.
- **أخطاء الشبكة:** تحقق من اتصالك وحاول مرة أخرى.
- **روابط غير صالحة:** تأكد من صحة الرابط وأنه من منصة مدعومة.
- **محتوى Premium:** يتطلب اشتراك يوتيوب بريميوم.
- **حظر حقوق الملكية:** المحتوى محظور بسبب قيود حقوق النشر.
- **ملفات الفيديو والصوت منفصلة بعد التحميل:** يحدث هذا عند فقدان FFmpeg أو عدم اكتشافه. يتطلب YTSage برنامج FFmpeg لدمج مسارات الفيديو والصوت عالية الجودة.
- **الحل:** تأكد من تثبيت FFmpeg وإضافته لـ PATH النظام. لمستخدمي ويندوز، الخيار الأسهل هو تحميل ملف `YTSage-v<version>-ffmpeg.exe` الذي يأتي مع FFmpeg مدمجًا.
---
#### 🛡️ تنبيه Windows Defender / الأنتي فيروس
قد تصنف بعض برامج الحماية ملفات `.exe` كبرمجيات ضارة (False Positive). هذا **عيب معروف** في التطبيقات المحزمة.
**لماذا يحدث هذا:**
- أساليب الكشف في مضادات الفيروسات قد تعتبر الملفات المحزمة مشبوهة خطأً.
**البدائل الآمنة:**
- ✅ **استخدم تثبيت pip:** `pip install ytsage` (موصى به)
- ✅ **البناء من المصدر:** باتباع هذا [الدليل](../.github/CI_CD_README.md)
- ✅ **إضافة التطبيق للقائمة البيضاء** في برنامج الحماية لديك.
#### 🍎 macOS: "The app is damaged and cant be opened"
إذا واجهت هذا الخطأ على macOS Sonoma أو أحدث، يجب عليك إزالة سمة الحجر الصحي.
1. **افتح الـ Terminal** (يمكنك البحث عنه عبر Spotlight).
2. **اكتب الأمر التالي** ولكن **لا تضغط** Enter بعد. تأكد من وجود مسافة في النهاية:
```bash
xattr -d com.apple.quarantine
```
3. **اسحب ملف `YTSage.app`** من نافذة Finder وأفلته في نافذة الـ Terminal. سيقوم بلصق المسار الصحيح تلقائيًا.
4. **اضغط Enter** لتنفيذ الأمر.
5. **حاول فتح YTSage.app مجددًا.** يجب أن يعمل الآن بشكل صحيح.
---
#### **مواقع الإعدادات (للمتقدمين)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 الدعم
إذا وفر لك YTSage الوقت، يرجى التفكير في دعم المشروع. يساعد الدعم في تغطية وقت التطوير، الاختبار على المنصات المختلفة، والتحسينات المستقبلية.
- GitHub Sponsors: https://github.com/sponsors/oop7
- رابط الدعم متاح أيضًا داخل التطبيق عبر نافذة "About".
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contributing"></a>
## 👥 المساهمة
نرحب بالمساهمات! إليك كيف يمكنك المساعدة:
1. 🍴 عمل Fork للمستودع
2. 🌿 إنشاء فرع جديد للميزة:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 حفظ التغييرات:
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 رفع التغييرات:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 فتح Pull Request
### 🌍 المساهمة في الترجمة
- حدث ملف README المترجم (مثل `readme-translations/README.ar.md`)
- حافظ على تزامن نصوص التطبيق بتعديل `ytsage/languages/<code>.json`
- إذا كانت لغتك غير موجودة، ابدأ من `README.md` وأنشئ ملفًا جديدًا باسم `README.<code>.md`
<details>
<summary>📂 هيكل المشروع</summary>
## YTSage - هيكل المشروع
يوضح هذا المستند التنظيم الهيكلي لمجلدات YTSage.
### 📁 هيكل المشروع
```
YTSage/
├── 📁 .github/ # إعدادات GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # نماذج التذاكر
│ │ └── 🐛-bug-report.md # نموذج تقرير الأخطاء
│ ├─── 📁 workflows/ # سير عمل GitHub Actions
│ │ ├── build-linux.yml # بناء نسخة لينكس
│ │ ├── build-macos.yml # بناء نسخة ماك
│ │ │── build-windows.yml # بناء نسخة ويندوز
| | └── release-all.yml # سير عمل الإصدارات
│ └── 📄 CI_CD_README.md # توثيق CI/CD
├── 📁 branding/ # أصول العلامة التجارية (لقطات شاشة، SVG)
│ ├── 📁 icons/ # أيقونات التطبيق
│ ├── 📁 screenshots/ # لقطات شاشة للتوثيق
│ └── 📁 svg/ # أصول بصيغة SVG
├── 📄 LICENSE # ملف الرخصة
├── 📄 pyproject.toml # بيانات المشروع والاعتمادات
├── 📄 README.md # التوثيق الرئيسي
├── 📄 requirements.txt # اعتمادات بايثون (للمطورين)
└── 📁 ytsage/ # مجلد الأكواد المصدرية
├── 📁 assets/ # الأصول اللازمة للتشغيل
│ ├── 📁 Icon/ # أيقونات التطبيق
│ └── 📁 sound/ # ملفات صوتية
├── 📁 languages/ # ملفات الترجمة
│ ├── 📄 ar.json # الترجمة العربية
│ ├── 📄 de.json # الترجمة الألمانية
│ ├── 📄 en.json # الترجمة الإنجليزية
│ └── ... # لغات أخرى
├── 📁 core/ # المنطق البرمجي الأساسي
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # تكامل Deno
│ ├── 📄 ytsage_downloader.py # وظائف التحميل
│ ├── 📄 ytsage_ffmpeg.py # تكامل FFmpeg
│ ├── 📄 ytsage_utils.py # وظائف مساعدة
│ └── 📄 ytsage_yt_dlp.py # تكامل yt-dlp
├── 📁 gui/ # مكونات واجهة المستخدم
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # نافذة التطبيق الرئيسية
│ └── 📁 ytsage_gui_dialogs/ # كلاسات النوافذ الحوارية
├── 📁 utils/ # موديلات مساعدة
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # مدير الإعدادات
│ └── 📄 ytsage_logger.py # أداة تسجيل السجلات
├── 📄 __init__.py # نقطة دخول الحزمة
└── 📄 main.py # سكربت التشغيل الرئيسي
```
</details>
## ⭐️ سجل النجوم
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 الرخصة
هذا المشروع مرخص بموجب رخصة MIT - راجع ملف [LICENSE](../LICENSE) لمزيد من التفاصيل.
## 🙏 شكر وتقدير
<details>
<summary>عرض الشكر والتقدير</summary>
<div align="center">
<p>شكر كبير لكل من ساهم في هذا المشروع بفتح تذكرة لاقتراح تحسين أو الإبلاغ عن خطأ.</p>
<table>
<tr class="section"><th colspan="2">المكونات الأساسية</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>محرك التحميل</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>معالجة الوسائط</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>بيئة التشغيل لتكامل yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">المكتبات وإطارات العمل</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>إطار عمل الواجهة الرسومية</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>معالجة الصور</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>طلبات HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>إدارة الإصدارات والحزم</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>معالجة Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>تسجيل السجلات</td>
</tr>
<tr class="section"><th colspan="2">الأصول والمساهمون</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>صوت التنبيه</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>مساهم بالكود</td>
</tr>
</table>
</div>
</details>
## ⚠️ إخلاء مسؤولية
تُستخدم هذه الأداة للاستخدام الشخصي فقط. يرجى احترام شروط خدمة يوتيوب وحقوق منشئي المحتوى.
---
<div align="center">
صنع بـ ❤️ بواسطة [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Ein moderner YouTube-Downloader mit einer eleganten PySide6-Benutzeroberfläche.**
Downloaden Sie Videos in jeder Qualität, extrahieren Sie Audio, rufen Sie Untertitel ab und vieles mehr.
### 🌍 README-Sprachen
Englisch: [EN](../README.md)
| Arabisch: [AR](README.ar.md)
| Deutsch: [DE](README.de.md)
| Spanisch: [ES](README.es.md)
| Französisch: [FR](README.fr.md)
| Hindi: [HI](README.hi.md)
| Indonesisch: [ID](README.id.md)
| Italienisch: [IT](README.it.md)
| Japanisch: [JA](README.ja.md)
| Polnisch: [PL](README.pl.md)
| Portugiesisch: [PT](README.pt.md)
| Russisch: [RU](README.ru.md)
| Türkisch: [TR](README.tr.md)
| Chinesisch: [ZH](README.zh.md)
<p align="center">
<a href="#installation">Installation</a> •
<a href="#features">Funktionen</a> •
<a href="#usage">Bedienung</a> •
<a href="#screenshots">Screenshots</a> •
<a href="#troubleshooting">Fehlerbehebung</a> •
<a href="#sponsor">Sponsoren</a> •
<a href="#contributing">Mitwirken</a>
</p>
</div>
---
<a id="why-ytsage"></a>
## ❓ Warum YTSage?
YTSage wurde für Benutzer entwickelt, die einen **einfachen, aber leistungsstarken YouTube-Downloader** suchen. Im Gegensatz zu anderen Tools bietet es:
- Eine moderne und übersichtliche PySide6-Oberfläche
- Ein-Klick-Downloads für Video, Audio und Untertitel
- Erweiterte Funktionen wie SponsorBlock, Zusammenführen von Untertiteln und Playlist-Auswahl
- Optionaler generischer Modus (Generic Mode) für von yt-dlp unterstützte Seiten außerhalb von YouTube
- Plattformübergreifende Unterstützung und einfache Installation
<a id="features"></a>
## ✨ Funktionen
<div align="center">
| Basisfunktionen | Erweiterte Funktionen | Zusätzliche Funktionen |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Formattabelle | 🚫 SponsorBlock-Integration | 🎞️ FPS/HDR-Anzeige |
| 🎵 Audio-Extraktion | 📝 Untertitel-Auswahl & Merging | 🔄 Automatisches yt-dlp Update |
| ✨ Einfache Benutzeroberfläche | 💾 Beschreibung & Thumbnail speichern | 🛠️ FFmpeg/yt-dlp/Deno Erkennung |
| 📋 Playlist-Support & Selector | 🚀 Geschwindigkeitsbegrenzer | ⚙️ Benutzerdefinierte Befehle |
| 📑 Kapitel-Integration | ✂️ Videoabschnitte zuschneiden | 🍪 Cookie-Login |
| 📜 Download-Historie | 🔄 Release-Kanal Auswahl | 🌐 Proxy-Support |
| 🎚️ Audioformat-Konvertierung | 🎬 Videoformat-Einstellungen | 🆙 Integrierter Update-Tab |
| 🌍 Generischer Modus | 🔊 Audio-Normalisierung (EBU R128) | 🌍 In 14 Sprachen lokalisiert |
| 💾 Playlist-Export | ⚙️ Standardqualität & Untertitel | |
</div>
<a id="installation"></a>
## 🚀 Installation
### ⚡ Schnelle Installation (Empfohlen)
Installieren Sie YTSage über PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Bestehende Installation aktualisieren</summary>
```bash
pip install --upgrade ytsage
```
</details>
Starten Sie dann die Anwendung:
```bash
ytsage
```
### 📦 Vorgefertigte ausführbare Dateien
> [👉 Neuestes Release herunterladen](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Format | Beschreibung |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Standard-Installer |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Mit integriertem FFmpeg |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable Version, keine Installation erforderlich |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable Version mit FFmpeg, gepackt |
<details>
<summary>🛠️ Installationsschritte</summary>
1. **EXE-Installer (`.exe`)**: Doppelklicken Sie auf die Datei und folgen Sie dem Installationsassistenten.
2. **Portable Version (`.zip`)**: Entpacken Sie das Archiv an den gewünschten Ort und starten Sie `ytsage.exe`.
3. **Integriertes FFmpeg**: Wählen Sie die Versionen mit integriertem FFmpeg, falls FFmpeg nicht bereits auf Ihrem System installiert ist.
</details>
#### 🐧 Linux
| Format | Beschreibung |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian-Paket |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, portabel |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM-Paket |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle |
<details>
<summary>🛠️ Installationsschritte</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Repariert fehlende Abhängigkeiten, falls nötig
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Folgen Sie den Anweisungen auf Flathub oder führen Sie aus:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Format | Beschreibung |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Gepackte App für Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Disk Image Installer für Apple Silicon |
<details>
<summary>🛠️ Installationsschritte</summary>
- **DMG-Installer (`.dmg`)**: Doppelklick zum Mounten, dann `YTSage.app` in den Programme-Ordner ziehen.
- **App-Archiv (`.zip`)**: Zip entpacken und `YTSage.app` in den Programme-Ordner verschieben.
*Hinweis: Wenn Sie die Fehlermeldung "App ist beschädigt" erhalten, lesen Sie bitte den Abschnitt zur Fehlerbehebung für macOS weiter unten.*
</details>
---
<details>
<summary>💻 Manuelle Installation aus dem Quellcode</summary>
### 1. Repository klonen
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Abhängigkeiten installieren
#### ⚡ Mit uv
```bash
uv pip install .
```
#### 📦 Oder mit Standard-pip
```bash
pip install .
```
### 3. Anwendung starten
```bash
python -m ytsage.main
```
</details>
<a id="screenshots"></a>
## 📸 Screenshots
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Download-Einstellungen" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Playlist-Download" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Download-Einstellungen</em></td>
<td align="center"><em>Playlist-Download</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Audioformat-Auswahl" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Benutzerdefinierte Optionen" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Audioformat</em></td>
<td align="center"><em>Benutzerdefinierte Optionen</em></td>
</tr>
</table>
</div>
<a id="usage"></a>
## 📖 Bedienung
<details>
<summary>🎯 Grundlegende Bedienung</summary>
1. **YTSage starten**
2. **YouTube-URL einfügen** (oder die Schaltfläche "Paste URL" verwenden)
3. **Auf "Analyze" klicken**
4. **Format auswählen:**
- `Video` für Video-Downloads
- `Audio Only` für Audio-Extraktion
5. **Optionen wählen:**
- Untertitel aktivieren & Sprache auswählen
- Untertitel-Merging aktivieren
- Thumbnail speichern
- Sponsoren-Segmente entfernen
- Beschreibung speichern
- Kapitel integrieren
6. **Ausgabeverzeichnis wählen**
7. **Auf "Download" klicken**
> 💡 Das Standard-Download-Verzeichnis ist der "Downloads"-Ordner des Benutzers.
</details>
<details>
<summary>📋 Playlist-Download</summary>
1. **Playlist-URL einfügen**
2. **Auf "Analyze" klicken**
3. **Videos aus dem Playlist-Selector auswählen (optional, standardmäßig alle)**
4. **Gewünschtes Format/Qualität wählen**
5. **Auf "Download" klicken**
> 💡 Die Anwendung verwaltet die Download-Warteschlange automatisch, und Sie können Playlist-Einträge als `.txt`, `.csv`, `.m3u` oder `.json` exportieren.
</details>
<details>
<summary>🌍 Generischer Modus für Nicht-YouTube-Seiten</summary>
Verwenden Sie den generischen Modus (Generic Mode), wenn YTSage URLs von anderen von yt-dlp unterstützten Seiten wie Dailymotion, CBC Gem, TikTok und anderen akzeptieren soll.
So verwenden Sie ihn:
1. Öffnen Sie `Download Settings`.
2. Aktivieren Sie `Generic Mode`.
3. Fügen Sie eine unterstützte Video- oder Playlist-URL ein, die nicht von YouTube stammt.
4. Klicken Sie auf `Analyze`.
5. Wählen Sie ein Format und laden Sie es wie gewohnt herunter.
Hinweise:
- Der generische Modus ändert nur die URL-Validierung innerhalb von YTSage. Die Zielseite muss weiterhin von Ihrer installierten yt-dlp-Version unterstützt werden.
- Einige Seiten erfordern Cookies, Login, Proxy oder zusätzliche yt-dlp-Argumente je nach Extractor.
- Wenn eine Seite fehlschlägt, aktualisieren Sie zuerst yt-dlp über den integrierten Update-Tab, bevor Sie das Problem melden.
</details>
<details>
<summary>🧰 Medien- & Download-Optionen</summary>
- **Untertitel-Optionen:** Sprachen filtern und Untertitel in die Videodatei einbetten.
- **Untertitel-Merging:** Mergt Untertitel in die Videodatei für fest eingebrannte (hardcoded) Untertitel.
- **Beschreibung speichern:** Speichert die Videobeschreibung als Textdatei.
- **Thumbnail speichern:** Speichert das Video-Thumbnail als Bilddatei.
- **Kapitel integrieren:** Bettet Kapitelmarken als Metadaten für kompatible Videoplayer ein.
- **Sponsoren-Segmente entfernen:** Entfernt gesponserte Abschnitte aus dem Video mithilfe von SponsorBlock.
- **Video zuschneiden:** Laden Sie nur bestimmte Teile eines Videos herunter, indem Sie Zeitbereiche im Format `HH:MM:SS` angeben.
</details>
<details>
<summary>⚙️ Ausgabe- & Dateieinstellungen</summary>
- **Geschwindigkeitsbegrenzer:** Begrenzt die Download-Geschwindigkeit, z. B. `500K` für 500 KB/s.
- **Download-Pfad speichern:** Speichert den Standard-Download-Pfad für zukünftige Downloads. Verfügbar unter **Download Settings → Download Path**.
- **Standard-Videoauflösung:** Legen Sie Ihre bevorzugte Videoauflösung für die automatische Auswahl fest (z. B. 1080p, 720p). Verfügbar unter **Download Settings → Default Video Resolution**.
- **Standard-Untertitelsprachen:** Legen Sie Standard-Untertitelsprachen für die automatische Auswahl fest (kommagetrennt, z. B. `de,en`). Verfügbar unter **Download Settings → Default Subtitle Languages**.
- **Dateinamen-Format:** Passen Sie das Format des Ausgabedateinamen mit Variablen wie `%(title)s`, `%(uploader)s`, `%(playlist_index)s` und `%(resolution)s` an. Verfügbar unter **Download Settings → Filename Format**.
- **Ausgabeformat erzwingen:** Erzwingt Video-Downloads in ein bestimmtes Containerformat wie `mp4`, `webm` oder `mkv`. Verfügbar unter **Download Settings → Output Format Settings**.
- **Audioformat-Konvertierung:** Konvertiert reine Audio-Downloads in bevorzugte Formate wie `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis` oder `Best`. Verfügbar unter **Download Settings → Audio Format Settings**.
- **Audio-Normalisierung:** Standardisiert die Lautstärke für reine Audio-Downloads unter Verwendung von EBU R128.
- **Gleichzeitige Verbindungen:** Erhöhen Sie die Download-Geschwindigkeit erheblich, indem Sie Dateien in mehreren Fragmenten gleichzeitig herunterladen. Verfügbar unter **Download Settings → General → Concurrent Connections** (Standardmäßig 1, maximal 8-10 empfohlen, um IP-Sperren zu vermeiden).
</details>
<details>
<summary>🌐 Zugriff & Netzwerk</summary>
- **Cookie-Login:** Loggen Sie sich mit Cookies bei YouTube ein, um auf private Inhalte zuzugreifen.
Verwendung:
1. **Empfohlen:** Verwenden Sie die integrierte Option `Extract cookies from browser` in der App, wählen Sie dann Ihren Browser und optional ein Profil.
2. Alternativ können Sie Cookies manuell extrahieren:
a. Exportieren Sie Cookies aus Ihrem Browser mit einer Erweiterung wie [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Kopieren Sie die Cookies im Netscape-Format
c. Erstellen Sie eine Datei namens `cookies.txt` und fügen Sie die Cookies ein
d. Wählen Sie die Datei `cookies.txt` in der App aus
- **Proxy-Support:** Verwenden Sie einen Proxy-Server für Downloads, z. B. `http://<proxy-server>:<port>`
- **Generischer Modus:** Ermöglicht YTSage das Analysieren und Herunterladen von anderen von yt-dlp unterstützten Seiten. Aktivieren Sie dies unter **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Tools & Wartung</summary>
- **Benutzerdefinierte Befehle:** Greifen Sie auf erweiterte yt-dlp-Funktionen über Befehlszeilenargumente zu.
- **Update-Tab:** Verwalten Sie integrierte Update-Tools an einem Ort in den benutzerdefinierten Optionen:
- **yt-dlp Updates:** Suchen Sie nach Updates und wechseln Sie zwischen Stable- und Nightly-Release-Kanälen.
- **FFmpeg-Versionsprüfung:** Überprüfen Sie Ihre FFmpeg-Version und öffnen Sie Installationsanleitungen.
- **Deno-Updates:** Überprüfen und aktualisieren Sie die Deno-Laufzeitumgebung.
- **FFmpeg/yt-dlp/Deno Erkennung:** Erkennt Pfade und Versionen von FFmpeg, yt-dlp und Deno automatisch aus dem About-Dialog.
- **Download-Historie:** Zeigt vergangene Downloads mit Thumbnails und Status über die Schaltfläche **History** an.
</details>
<details>
<summary>🌍 Lokalisierung</summary>
YTSage unterstützt **14 Sprachen** für globale Barrierefreiheit. Wählen Sie Ihre bevorzugte Sprache unter **Custom Options → Language**.
### Unterstützte Sprachen
| Sprache | Code | Sprache | Code |
|----------|------|----------|------|
| 🇺🇸 Englisch | `en` | 🇪🇸 Spanisch | `es` |
| 🇸🇦 Arabisch | `ar` | 🇫🇷 Französisch | `fr` |
| 🇩🇪 Deutsch | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonesisch | `id` | 🇮🇹 Italienisch | `it` |
| 🇯🇵 Japanisch | `ja` | 🇵🇱 Polnisch | `pl` |
| 🇧🇷 Portugiesisch | `pt` | 🇷🇺 Russisch | `ru` |
| 🇹🇷 Türkisch | `tr` | 🇨🇳 Chinesisch | `zh` |
### README-Übersetzungen
| Sprache | Datei | Sprache | Datei |
|----------|------|----------|------|
| 🇺🇸 Englisch | [README.md](../README.md) | 🇪🇸 Spanisch | [README.es.md](README.es.md) |
| 🇸🇦 Arabisch | [README.ar.md](README.ar.md) | 🇫🇷 Französisch | [README.fr.md](README.fr.md) |
| 🇩🇪 Deutsch | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonesisch | [README.id.md](README.id.md) | 🇮🇹 Italienisch | [README.it.md](README.it.md) |
| 🇯🇵 Japanisch | [README.ja.md](README.ja.md) | 🇵🇱 Polnisch | [README.pl.md](README.pl.md) |
| 🇧🇷 Portugiesisch | [README.pt.md](README.pt.md) | 🇷🇺 Russisch | [README.ru.md](README.ru.md) |
| 🇹🇷 Türkisch | [README.tr.md](README.tr.md) | 🇨🇳 Chinesisch | [README.zh.md](README.zh.md) |
> 💡 **Möchten Sie bei einer Übersetzung helfen?** Schauen Sie in den Abschnitt [Mitwirken](#contributing), um uns zu helfen, weitere Sprachen hinzuzufügen!
</details>
<a id="troubleshooting"></a>
## 🛠️ Fehlerbehebung
<details>
<summary>Klicken Sie hier, um häufige Probleme und Lösungen anzuzeigen</summary>
- **Formattabelle wird nicht angezeigt:** Aktualisieren Sie yt-dlp auf die neueste Version und wechseln Sie zu yt-dlp nightly.
- **Download schlägt fehl:** Überprüfen Sie Ihre Internetverbindung und stellen Sie sicher, dass das Video verfügbar ist.
- **Spezifische Download-Fehler:**
- **Private Videos:** Verwenden Sie Cookie-Authentifizierung, um auf private Inhalte zuzugreifen.
- **Videos mit Altersbeschränkung:** Loggen Sie sich in Ihr YouTube-Konto ein, um altersbeschränkte Videos anzusehen.
- **Geoblockierte Videos:** Erwägen Sie die Verwendung eines VPN, um regionale Einschränkungen zu umgehen.
- **Gelöschte Videos:** Das Video ist auf YouTube nicht mehr verfügbar.
- **Livestreams:** Livestreams können während der Übertragung nicht heruntergeladen werden; warten Sie, bis der Stream beendet ist.
- **Netzwerkfehler:** Überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.
- **Ungültige URLs:** Stellen Sie sicher, dass die URL korrekt ist und von einer unterstützten Plattform stammt.
- **Premium-Inhalte:** Erfordert ein YouTube-Premium-Abonnement.
- **Urheberrechtssperren:** Der Inhalt ist aufgrund von Urheberrechtsbeschränkungen gesperrt.
- **Video- und Audiodateien sind nach dem Download getrennt:** Dies passiert, wenn FFmpeg fehlt oder nicht erkannt wird. YTSage benötigt FFmpeg, um hochwertige Video- und Audiostreams zusammenzuführen.
- **Lösung:** Stellen Sie sicher, dass FFmpeg installiert und im PATH Ihres Systems zugänglich ist. Für Windows-Benutzer ist die einfachste Option, die Datei `YTSage-v<version>-ffmpeg.exe` herunterzuladen, die FFmpeg integriert hat.
---
#### 🛡️ Windows Defender / Antivirus Warnung
Einige Antivirenprogramme können `.exe`-Dateien als Fehlalarm (False Positive) markieren. Dies ist eine **bekannte Einschränkung** von gepackten Anwendungen.
**Warum das passiert:**
- Antiviren-Heuristiken können gepackte ausführbare Dateien fälschlicherweise als verdächtig identifizieren.
**Sichere Alternativen:**
- ✅ **Verwenden Sie die pip-Installation:** `pip install ytsage` (Empfohlen)
- ✅ **Aus dem Quellcode bauen**: Folgen Sie diesem [Leitfaden](../.github/CI_CD_README.md)
- ✅ **App auf die Whitelist setzen** in Ihrer Antiviren-Software.
#### 🍎 macOS: "Die App ist beschädigt und kann nicht geöffnet werden"
Wenn dieser Fehler unter macOS Sonoma oder neuer auftritt, müssen Sie das Quarantäne-Attribut entfernen.
1. **Öffnen Sie das Terminal** (Sie finden es über Spotlight).
2. **Geben Sie den folgenden Befehl ein**, aber drücken Sie noch **nicht** Enter. Stellen Sie sicher, dass am Ende ein Leerzeichen steht:
```bash
xattr -d com.apple.quarantine
```
3. **Ziehen Sie die Datei `YTSage.app`** aus Ihrem Finder-Fenster direkt in das Terminal-Fenster. Dadurch wird der korrekte Dateipfad automatisch eingefügt.
4. **Drücken Sie Enter**, um den Befehl auszuführen.
5. **Versuchen Sie erneut, YTSage.app zu öffnen.** Sie sollte nun korrekt starten.
---
#### **Speicherorte der Konfiguration (Erweitert)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Sponsoren
Wenn YTSage Ihnen Zeit spart, ziehen Sie bitte in Erwägung, das Projekt zu sponsern. Sponsoring hilft, die Entwicklungszeit, Tests auf allen Plattformen und zukünftige Verbesserungen zu finanzieren.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Der Sponsoring-Link ist auch direkt in der App über den About-Dialog verfügbar.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contributing"></a>
## 👥 Mitwirken
Beiträge sind herzlich willkommen! So können Sie helfen:
1. 🍴 Forken Sie das Repository
2. 🌿 Erstellen Sie einen Feature-Branch:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 Committen Sie Ihre Änderungen:
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 Pushen Sie in den Branch:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 Öffnen Sie einen Pull Request
### 🌍 Zu Übersetzungen beitragen
- Aktualisieren Sie die entsprechende lokalisierte README-Datei (z. B. `readme-translations/README.de.md`)
- Halten Sie die App-Strings synchron, indem Sie `ytsage/languages/<code>.json` bearbeiten
- Wenn Ihre Sprache fehlt, beginnen Sie mit der `README.md` und erstellen Sie `README.<code>.md`
<details>
<summary>📂 Projektstruktur</summary>
## YTSage - Projektstruktur
Dieses Dokument beschreibt die organisierte Ordnerstruktur von YTSage.
### 📁 Projektstruktur
```
YTSage/
├── 📁 .github/ # GitHub-Konfiguration
│ ├── 📁 ISSUE_TEMPLATE/ # Ticket-Vorlagen
│ │ └── 🐛-bug-report.md # Bug-Report-Vorlage
│ ├─── 📁 workflows/ # GitHub Actions Workflows
│ │ ├── build-linux.yml # Linux Build Workflow
│ │ ├── build-macos.yml # macOS Build Workflow
│ │ │── build-windows.yml # Windows Build Workflow
| | └── release-all.yml # Master Release Workflow
│ └── 📄 CI_CD_README.md # CI/CD-Dokumentation
├── 📁 branding/ # Branding-Assets (Screenshots, SVGs)
│ ├── 📁 icons/ # App-Icons
│ ├── 📁 screenshots/ # Screenshots für die Dokumentation
│ └── 📁 svg/ # SVG-Assets
├── 📄 LICENSE # Lizenzdatei
├── 📄 pyproject.toml # Projekt-Metadaten und Abhängigkeiten
├── 📄 README.md # Projekt-Dokumentation
├── 📄 requirements.txt # Python-Abhängigkeiten (dev)
└── 📁 ytsage/ # Quellcode-Paket
├── 📁 assets/ # Laufzeit-Assets
│ ├── 📁 Icon/ # App-Icons
│ └── 📁 sound/ # Audiodateien
├── 📁 languages/ # Lokalisierungsdateien
│ ├── 📄 ar.json # Arabische Übersetzung
│ ├── 📄 de.json # Deutsche Übersetzung
│ ├── 📄 en.json # Englische Übersetzung
│ └── ... # Weitere Sprachen
├── 📁 core/ # Hauptgeschäftslogik
│ ├── 📄 __init__.py # Core-Paket-Initialisierung
│ ├── 📄 ytsage_deno.py # Deno-Integration
│ ├── 📄 ytsage_downloader.py # Download-Funktionalität
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg-Integration
│ ├── 📄 ytsage_utils.py # Hilfsfunktionen
│ └── 📄 ytsage_yt_dlp.py # yt-dlp-Integration
├── 📁 gui/ # Benutzeroberflächen-Komponenten
│ ├── 📄 __init__.py # GUI-Paket-Initialisierung
│ ├── 📄 ytsage_gui_main.py # Hauptfenster der App
│ └── 📁 ytsage_gui_dialogs/ # Dialog-Klassen
├── 📁 utils/ # Hilfsmodule
│ ├── 📄 __init__.py # Utils-Paket-Initialisierung
│ ├── 📄 ytsage_config_manager.py # Konfigurationsverwaltung
│ └── 📄 ytsage_logger.py # Logging-Tool
├── 📄 __init__.py # Paket-Einstiegspunkt
└── 📄 main.py # Hauptskript zur Ausführung
```
</details>
## ⭐️ Star History
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Lizenz
Dieses Projekt steht unter der MIT-Lizenz weitere Details finden Sie in der Datei [LICENSE](../LICENSE).
## 🙏 Danksagungen
<details>
<summary>Danksagungen anzeigen</summary>
<div align="center">
<p>Ein großes Dankeschön an alle, die zu diesem Projekt beigetragen haben, indem sie Tickets für Verbesserungsvorschläge oder Bug-Reports geöffnet haben.</p>
<table>
<tr class="section"><th colspan="2">Kernkomponenten</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Download-Engine</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Medienverarbeitung</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime für yt-dlp Integration</td>
</tr>
<tr class="section"><th colspan="2">Bibliotheken & Frameworks</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI-Framework</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Bildverarbeitung</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>HTTP-Anfragen</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Versionsverwaltung & Packaging</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown-Rendering</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td>
</tr>
<tr class="section"><th colspan="2">Assets & Mitwirkende</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>Benachrichtigungston</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Code-Contributor</td>
</tr>
</table>
</div>
</details>
## ⚠️ Haftungsausschluss
Dieses Tool ist nur für den persönlichen Gebrauch bestimmt. Bitte respektieren Sie die Nutzungsbedingungen von YouTube und die Rechte der Content-Ersteller.
---
<div align="center">
Erstellt mit ❤️ von [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Un moderno descargador de YouTube con una interfaz PySide6 elegante.**
Descarga videos en cualquier calidad, extrae audio, obtén subtítulos y más.
### 🌍 Idiomas del README
Inglés: [EN](../README.md)
| Árabe: [AR](README.ar.md)
| Alemán: [DE](README.de.md)
| Español: [ES](README.es.md)
| Francés: [FR](README.fr.md)
| Hindi: [HI](README.hi.md)
| Indonesio: [ID](README.id.md)
| Italiano: [IT](README.it.md)
| Japonés: [JA](README.ja.md)
| Polaco: [PL](README.pl.md)
| Portugués: [PT](README.pt.md)
| Ruso: [RU](README.ru.md)
| Turco: [TR](README.tr.md)
| Chino: [ZH](README.zh.md)
<p align="center">
<a href="#installation">Instalación</a> •
<a href="#features">Características</a> •
<a href="#usage">Uso</a> •
<a href="#screenshots">Capturas de pantalla</a> •
<a href="#troubleshooting">Solución de problemas</a> •
<a href="#sponsor">Patrocinar</a> •
<a href="#contributing">Contribuir</a>
</p>
</div>
---
<a id="why-ytsage"></a>
## ❓ ¿Por qué YTSage?
YTSage está diseñado para usuarios que desean un **descargador de YouTube simple pero potente**. A diferencia de otras herramientas, ofrece:
- Interfaz PySide6 moderna y limpia
- Descarga con un solo clic para video, audio y subtítulos
- Funciones avanzadas como SponsorBlock, fusión de subtítulos y selección de listas de reproducción
- Modo genérico (Generic Mode) opcional para sitios compatibles con yt-dlp más allá de YouTube
- Soporte multiplataforma e instalación sencilla
<a id="features"></a>
## ✨ Características
<div align="center">
| Características básicas | Características avanzadas | Características adicionales |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Tabla de formatos | 🚫 Integración de SponsorBlock | 🎞️ Pantalla FPS/HDR |
| 🎵 Extracción de audio | 📝 Selección y fusión de subtítulos | 🔄 Actualización automática de yt-dlp |
| ✨ Interfaz de usuario simple | 💾 Guarda descripción y miniatura | 🛠️ Detección de FFmpeg/yt-dlp/Deno |
| 📋 Soporte y selector de listas de reproducción | 🚀 Limitador de velocidad | ⚙️ Comandos personalizados |
| 📑 Integración de capítulos | ✂️ Recorte de secciones de video | 🍪 Inicio de sesión con cookies |
| 📜 Historial de descargas | 🔄 Selección de canal de lanzamiento | 🌐 Soporte de proxy |
| 🎚️ Conversión de formato de audio | 🎬 Configuración de formato de video | 🆙 Pestaña de actualización integrada |
| 🌍 Modo genérico | 🔊 Normalización de audio (EBU R128) | 🌍 Localización en 14 idiomas |
| 💾 Exportación de listas de reproducción | ⚙️ Calidad y subtítulos predeterminados | |
</div>
<a id="installation"></a>
## 🚀 Instalación
### ⚡ Instalación rápida (Recomendado)
Instala YTSage vía PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Actualizar instalación existente</summary>
```bash
pip install --upgrade ytsage
```
</details>
Luego, ejecuta la aplicación:
```bash
ytsage
```
### 📦 Ejecutables precompilados
> [👉 Descargar el último lanzamiento](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Formato | Descripción |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Instalador estándar |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Con FFmpeg incorporado |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Versión portable, no requiere instalación |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable con FFmpeg, comprimido |
<details>
<summary>🛠️ Pasos de instalación</summary>
1. **Instalador EXE (`.exe`)**: Haz doble clic en el archivo y sigue el asistente de configuración.
2. **Versión portable (`.zip`)**: Extrae el archivo en el lugar deseado y ejecuta `ytsage.exe`.
3. **FFmpeg incorporado**: Elige las versiones con FFmpeg incorporado si no tienes FFmpeg instalado en tu sistema.
</details>
#### 🐧 Linux
| Formato | Descripción |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Paquete Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, portable |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Paquete RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle |
<details>
<summary>🛠️ Pasos de instalación</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Repara dependencias faltantes si es necesario
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Sigue las instrucciones en Flathub o ejecuta:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Formato | Descripción |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Aplicación comprimida para Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Instalador de imagen de disco para Apple Silicon |
<details>
<summary>🛠️ Pasos de instalación</summary>
- **Instalador DMG (`.dmg`)**: Haz doble clic para montar, luego arrastra `YTSage.app` a tu carpeta de Aplicaciones.
- **Archivo de aplicación (`.zip`)**: Extrae el zip y mueve `YTSage.app` a tu carpeta de Aplicaciones.
*Nota: Si encuentras el error "La aplicación está dañada", consulta la sección de solución de problemas de macOS a continuación.*
</details>
---
<details>
<summary>💻 Instalación manual desde el código fuente</summary>
### 1. Clonar el repositorio
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Instalar dependencias
#### ⚡ Con uv
```bash
uv pip install .
```
#### 📦 O con pip estándar
```bash
pip install .
```
### 3. Ejecutar la aplicación
```bash
python -m ytsage.main
```
</details>
<a id="screenshots"></a>
## 📸 Capturas de pantalla
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Ajustes de descarga" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Descarga de lista de reproducción" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Ajustes de descarga</em></td>
<td align="center"><em>Descarga de lista de reproducción</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Selección de formato de audio" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Opción personalizada" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Formato de audio</em></td>
<td align="center"><em>Opción personalizada</em></td>
</tr>
</table>
</div>
<a id="usage"></a>
## 📖 Uso
<details>
<summary>🎯 Uso básico</summary>
1. **Lanza YTSage**
2. **Pega la URL de YouTube** (o usa el botón "Paste URL")
3. **Haz clic en "Analyze"**
4. **Selecciona el formato:**
- `Video` para descargas de video
- `Audio Only` para extracción de audio
5. **Elige opciones:**
- Activar subtítulos y elegir idioma
- Activar fusión de subtítulos
- Guardar miniatura
- Eliminar segmentos patrocinados
- Guardar descripción
- Integrar capítulos
6. **Selecciona directorio de salida**
7. **Haz clic en "Download"**
> 💡 El directorio de descarga predeterminado es la carpeta "Downloads" del usuario.
</details>
<details>
<summary>📋 Descarga de listas de reproducción</summary>
1. **Pega la URL de la lista de reproducción**
2. **Haz clic en "Analyze"**
3. **Selecciona videos del selector de listas (opcional, todos por defecto)**
4. **Elige formato/calidad deseada**
5. **Haz clic en "Download"**
> 💡 La aplicación gestiona las colas de descarga automáticamente, y puedes exportar entradas de la lista como archivos `.txt`, `.csv`, `.m3u` o `.json`.
</details>
<details>
<summary>🌍 Modo genérico para sitios que no son YouTube</summary>
Usa el modo genérico (Generic Mode) cuando desees que YTSage acepte URLs de sitios compatibles con yt-dlp, como Dailymotion, CBC Gem, TikTok y otros.
Cómo usarlo:
1. Abre `Download Settings`.
2. Activa `Generic Mode`.
3. Pega una URL de video o lista de reproducción compatible que no sea de YouTube.
4. Haz clic en `Analyze`.
5. Elige el formato y descarga como de costumbre.
Notas:
- El modo genérico solo cambia la validación de la URL dentro de YTSage. El sitio debe ser compatible con tu versión instalada de yt-dlp.
- Algunos sitios requieren cookies, inicio de sesión, proxy o argumentos adicionales de yt-dlp dependiendo del extractor.
- Si un sitio falla, actualiza primero yt-dlp desde la pestaña de actualización integrada antes de informar del problema.
</details>
<details>
<summary>🧰 Opciones de medios y descarga</summary>
- **Opciones de subtítulos:** Filtra idiomas e incrusta subtítulos en el archivo de video.
- **Fusión de subtítulos:** Fusiona subtítulos en el archivo de video para subtítulos fijos (hardcoded).
- **Guardar descripción:** Guarda la descripción del video como un archivo de texto.
- **Guardar miniatura:** Guarda la miniatura del video como un archivo de imagen.
- **Integrar capítulos:** Incluye marcas de capítulo como metadatos para reproductores de video compatibles.
- **Eliminar segmentos patrocinados:** Elimina segmentos patrocinados del video usando SponsorBlock.
- **Recortar video:** Descarga solo partes específicas de un video especificando rangos de tiempo en formato `HH:MM:SS`.
</details>
<details>
<summary>⚙️ Ajustes de salida y archivos</summary>
- **Limitador de velocidad:** Limita la velocidad de descarga, p. ej., `500K` para 500 KB/s.
- **Guardar ruta de descarga:** Guarda la ruta de descarga predeterminada para futuras descargas. Disponible en **Download Settings → Download Path**.
- **Resolución de video predeterminada:** Establece tu resolución de video preferida para selección automática (p. ej., 1080p, 720p). Disponible en **Download Settings → Default Video Resolution**.
- **Idiomas de subtítulos predeterminados:** Establece idiomas de subtítulos para selección automática (separados por comas, p. ej., `es,en`). Disponible en **Download Settings → Default Subtitle Languages**.
- **Formato de nombre de archivo:** Personaliza el formato del nombre de archivo usando variables como `%(title)s`, `%(uploader)s`, `%(playlist_index)s` y `%(resolution)s`. Disponible en **Download Settings → Filename Format**.
- **Forzar formato de salida:** Fuerza las descargas de video a un formato de contenedor específico como `mp4`, `webm` o `mkv`. Disponible en **Download Settings → Output Format Settings**.
- **Conversión de formato de audio:** Convierte descargas de solo audio a formatos preferidos como `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis` o `Best`. Disponible en **Download Settings → Audio Format Settings**.
- **Normalización de audio:** Estandariza el volumen para descargas de solo audio usando EBU R128.
- **Conexiones simultáneas:** Aumenta drásticamente la velocidad de descarga descargando archivos en múltiples fragmentos a la vez. Disponible en **Download Settings → General → Concurrent Connections** (1 por defecto, máximo recomendado 8-10 para evitar bloqueos por IP).
</details>
<details>
<summary>🌐 Acceso y red</summary>
- **Inicio de sesión con cookies:** Inicia sesión en YouTube usando cookies para acceder a contenido privado.
Cómo usarlo:
1. **Recomendado:** Usa la opción integrada `Extract cookies from browser` en la aplicación, luego selecciona tu navegador y opcionalmente un perfil.
2. Alternativamente, extrae las cookies manualmente:
a. Exporta cookies de tu navegador usando una extensión como [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Copia las cookies en formato Netscape
c. Crea un archivo llamado `cookies.txt` y pega las cookies
d. Selecciona el archivo `cookies.txt` en la aplicación
- **Soporte de proxy:** Usa un servidor proxy para las descargas, p. ej., `http://<servidor-proxy>:<puerto>`
- **Modo genérico:** Permite a YTSage analizar y descargar desde sitios que no son YouTube compatibles con yt-dlp. Actívalo en **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Herramientas y mantenimiento</summary>
- **Comandos personalizados:** Accede a funciones avanzadas de yt-dlp mediante argumentos de línea de comandos.
- **Pestaña de actualización:** Gestiona herramientas de actualización desde un solo lugar en Opciones personalizadas:
- **Actualizaciones de yt-dlp:** Busca actualizaciones y cambia entre canales estable y nightly.
- **Verificador de versión de FFmpeg:** Verifica tu versión de FFmpeg y abre guías de instalación.
- **Actualizaciones de Deno:** Verifica y actualiza el motor de ejecución Deno.
- **Detección de FFmpeg/yt-dlp/Deno:** Detecta automáticamente rutas y versiones de FFmpeg, yt-dlp y Deno desde el diálogo Acerca de.
- **Historial de descargas:** Visualiza descargas pasadas con miniaturas y estados desde el botón **History**.
</details>
<details>
<summary>🌍 Localización</summary>
YTSage soporta **14 idiomas** para accesibilidad global. Selecciona tu idioma preferido en **Custom Options → Language**.
### Idiomas soportados
| Idioma | Código | Idioma | Código |
|----------|------|----------|------|
| 🇺🇸 Inglés | `en` | 🇪🇸 Español | `es` |
| 🇸🇦 Árabe | `ar` | 🇫🇷 Francés | `fr` |
| 🇩🇪 Alemán | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonesio | `id` | 🇮🇹 Italiano | `it` |
| 🇯🇵 Japonés | `ja` | 🇵🇱 Polaco | `pl` |
| 🇧🇷 Portugués | `pt` | 🇷🇺 Ruso | `ru` |
| 🇹🇷 Turco | `tr` | 🇨🇳 Chino | `zh` |
### Traducciones del README
| Idioma | Archivo | Idioma | Archivo |
|----------|------|----------|------|
| 🇺🇸 Inglés | [README.md](../README.md) | 🇪🇸 Español | [README.es.md](README.es.md) |
| 🇸🇦 Árabe | [README.ar.md](README.ar.md) | 🇫🇷 Francés | [README.fr.md](README.fr.md) |
| 🇩🇪 Alemán | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonesio | [README.id.md](README.id.md) | 🇮🇹 Italiano | [README.it.md](README.it.md) |
| 🇯🇵 Japonés | [README.ja.md](README.ja.md) | 🇵🇱 Polaco | [README.pl.md](README.pl.md) |
| 🇧🇷 Portugués | [README.pt.md](README.pt.md) | 🇷🇺 Ruso | [README.ru.md](README.ru.md) |
| 🇹🇷 Turco | [README.tr.md](README.tr.md) | 🇨🇳 Chino | [README.zh.md](README.zh.md) |
> 💡 **¿Quieres contribuir con una traducción?** ¡Consulta la sección de [Contribución](#contributing) para ayudarnos a añadir más idiomas!
</details>
<a id="troubleshooting"></a>
## 🛠️ Solución de problemas
<details>
<summary>Haz clic para ver problemas comunes y soluciones</summary>
- **La tabla de formatos no aparece:** Actualiza yt-dlp a la última versión y cambia a yt-dlp nightly.
- **Fallo de descarga:** Verifica tu conexión a internet y asegúrate de que el video esté disponible.
- **Errores de descarga específicos:**
- **Videos privados:** Usa autenticación por cookies para acceder a contenido privado.
- **Contenido con restricción de edad:** Inicia sesión en tu cuenta de YouTube para ver videos restringidos.
- **Videos bloqueados geográficamente:** Considera usar una VPN para saltar restricciones regionales.
- **Videos eliminados:** El video ya no está disponible en YouTube.
- **Transmisiones en vivo:** Los directos no se pueden descargar; espera a que la transmisión termine.
- **Errores de red:** Verifica tu conexión a internet e inténtalo de nuevo.
- **URLs no válidas:** Asegúrate de que la URL sea correcta y de una plataforma compatible.
- **Contenido Premium:** Requiere suscripción a YouTube Premium.
- **Bloqueos por copyright:** El contenido está bloqueado por restricciones de derechos de autor.
- **Archivos de video y audio separados tras descarga:** Esto sucede cuando falta FFmpeg o no se detecta. YTSage requiere FFmpeg para fusionar flujos de video y audio de alta calidad.
- **Solución:** Asegúrate de que FFmpeg esté instalado y accesible en el PATH de tu sistema. Para usuarios de Windows, la opción más fácil es descargar el archivo `YTSage-v<version>-ffmpeg.exe`, que incluye FFmpeg.
---
#### 🛡️ Advertencia de Windows Defender / Antivirus
Algunos programas antivirus pueden marcar archivos `.exe` como falsos positivos. Esta es una **limitación conocida** de las aplicaciones empaquetadas.
**Por qué sucede esto:**
- La heurística de los antivirus puede identificar erróneamente ejecutables empaquetados como sospechosos.
**Alternativas seguras:**
- ✅ **Usa la instalación de pip:** `pip install ytsage` (Recomendado)
- ✅ **Construye desde el código fuente**: siguiendo esta [guía](../.github/CI_CD_README.md)
- ✅ **Añade la aplicación a la lista blanca** en tu software antivirus.
#### 🍎 macOS: "La aplicación está dañada y no se puede abrir"
Si ves este error en macOS Sonoma o más reciente, necesitas eliminar el atributo de cuarentena.
1. **Abre el Terminal** (puedes encontrarlo usando Spotlight).
2. **Escribe el siguiente comando** pero **no presiones** Enter todavía. Asegúrate de incluir el espacio al final:
```bash
xattr -d com.apple.quarantine
```
3. **Arrastra el archivo `YTSage.app`** desde tu ventana de Finder y suéltalo directamente en la ventana del Terminal. Esto pegará automáticamente la ruta correcta del archivo.
4. **Presiona Enter** para ejecutar el comando.
5. **Intenta abrir YTSage.app de nuevo.** Ahora debería lanzarse correctamente.
---
#### **Ubicaciones de configuración (Avanzado)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Patrocinar
Si YTSage te ahorra tiempo, considera patrocinar el proyecto. El patrocinio ayuda a cubrir tiempo de desarrollo, pruebas en todas las plataformas y futuras mejoras.
- GitHub Sponsors: https://github.com/sponsors/oop7
- El enlace de patrocinio también está disponible directamente en la aplicación a través del diálogo Acerca de.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contributing"></a>
## 👥 Contribuir
¡Agradecemos las contribuciones! Aquí tienes cómo puedes ayudar:
1. 🍴 Haz un Fork del repositorio
2. 🌿 Crea tu rama de características:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 Haz commit de tus cambios:
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 Haz Push a la rama:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 Abre un Pull Request
### 🌍 Contribuir con traducciones
- Actualiza el archivo README localizado correspondiente (p. ej., `readme-translations/README.es.md`)
- Mantén las cadenas de la aplicación sincronizadas editando `ytsage/languages/<code>.json`
- Si falta tu idioma, comienza desde `README.md` y crea `README.<code>.md`
<details>
<summary>📂 Estructura del proyecto</summary>
## YTSage - Estructura del proyecto
Este documento describe la estructura de carpetas organizada de YTSage.
### 📁 Estructura del proyecto
```
YTSage/
├── 📁 .github/ # Configuración de GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Plantillas de problemas
│ │ └── 🐛-bug-report.md # Plantilla de informe de errores
│ ├─── 📁 workflows/ # Flujos de trabajo de GitHub Actions
│ │ ├── build-linux.yml # Flujo de construcción para Linux
│ │ ├── build-macos.yml # Flujo de construcción para macOS
│ │ │── build-windows.yml # Flujo de construcción para Windows
| | └── release-all.yml # Flujo de liberación maestra
│ └── 📄 CI_CD_README.md # Documentación de CI/CD
├── 📁 branding/ # Activos de marca (Capturas de pantalla, SVGs)
│ ├── 📁 icons/ # Iconos de la aplicación
│ ├── 📁 screenshots/ # Capturas de pantalla para documentación
│ └── 📁 svg/ # Activos SVG
├── 📄 LICENSE # Archivo de licencia
├── 📄 pyproject.toml # Metadatos del proyecto y dependencias
├── 📄 README.md # Documentación del proyecto
├── 📄 requirements.txt # Dependencias de Python (dev)
└── 📁 ytsage/ # Paquete de código fuente
├── 📁 assets/ # Activos en tiempo de ejecución
│ ├── 📁 Icon/ # Iconos de la aplicación
│ └── 📁 sound/ # Archivos de audio
├── 📁 languages/ # Archivos de localización
│ ├── 📄 ar.json # Traducción al árabe
│ ├── 📄 de.json # Traducción al alemán
│ ├── 📄 en.json # Traducción al inglés
│ └── ... # Otros idiomas
├── 📁 core/ # Lógica de negocio principal
│ ├── 📄 __init__.py # Init del paquete core
│ ├── 📄 ytsage_deno.py # Integración con Deno
│ ├── 📄 ytsage_downloader.py # Funcionalidad de descarga
│ ├── 📄 ytsage_ffmpeg.py # Integración con FFmpeg
│ ├── 📄 ytsage_utils.py # Funciones de utilidad
│ └── 📄 ytsage_yt_dlp.py # Integración con yt-dlp
├── 📁 gui/ # Componentes de interfaz de usuario
│ ├── 📄 __init__.py # Init del paquete GUI
│ ├── 📄 ytsage_gui_main.py # Ventana principal de la aplicación
│ └── 📁 ytsage_gui_dialogs/ # Clases de diálogos
├── 📁 utils/ # Módulos de utilidad
│ ├── 📄 __init__.py # Init del paquete utils
│ ├── 📄 ytsage_config_manager.py # Gestión de configuración
│ └── 📄 ytsage_logger.py # Utilidad de registro
├── 📄 __init__.py # Punto de entrada del paquete
└── 📄 main.py # Script de ejecución principal
```
</details>
## ⭐️ Historial de estrellas
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Licencia
Este proyecto está bajo la Licencia MIT - mira el archivo [LICENSE](../LICENSE) para más detalles.
## 🙏 Agradecimientos
<details>
<summary>Ver agradecimientos</summary>
<div align="center">
<p>Muchas gracias a todos los que han contribuido a este proyecto abriendo un problema para sugerir una mejora o informar de un error.</p>
<table>
<tr class="section"><th colspan="2">Componentes principales</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Motor de descarga</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Procesamiento de medios</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime para integración con yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Librerías y Frameworks</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>Framework de GUI</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Procesamiento de imágenes</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>Solicitudes HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Gestión de versiones y empaquetado</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Renderizado de Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td>
</tr>
<tr class="section"><th colspan="2">Activos y Colaboradores</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>Sonido de notificación</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Colaborador de código</td>
</tr>
</table>
</div>
</details>
## ⚠️ Descargo de responsabilidad
Esta herramienta es solo para uso personal. Por favor, respeta los términos de servicio de YouTube y los derechos de los creadores de contenido.
---
<div align="center">
Hecho con ❤️ por [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Un téléchargeur YouTube moderne avec une interface PySide6 épurée.**
Téléchargez des vidéos dans n'importe quelle qualité, extrayez l'audio, récupérez les sous-titres, et plus encore.
### 🌍 Langues du README
Anglais : [EN](../README.md)
| Arabe : [AR](README.ar.md)
| Allemand : [DE](README.de.md)
| Espagnol : [ES](README.es.md)
| Français : [FR](README.fr.md)
| Hindi : [HI](README.hi.md)
| Indonésien : [ID](README.id.md)
| Italien : [IT](README.it.md)
| Japonais : [JA](README.ja.md)
| Polonais : [PL](README.pl.md)
| Portugais : [PT](README.pt.md)
| Russe : [RU](README.ru.md)
| Turc : [TR](README.tr.md)
| Chinois : [ZH](README.zh.md)
<p align="center">
<a href="#installation">Installation</a> •
<a href="#features">Fonctionnalités</a> •
<a href="#usage">Utilisation</a> •
<a href="#screenshots">Captures d'écran</a> •
<a href="#troubleshooting">Dépannage</a> •
<a href="#sponsor">Sponsor</a> •
<a href="#contributing">Contribution</a>
</p>
</div>
---
<a id="why-ytsage"></a>
## ❓ Pourquoi YTSage ?
YTSage est conçu pour les utilisateurs qui recherchent un **téléchargeur YouTube simple mais puissant**. Contrairement à d'autres outils, il offre :
- Une interface PySide6 moderne et épurée
- Téléchargements en un clic pour la vidéo, l'audio et les sous-titres
- Fonctionnalités avancées comme SponsorBlock, la fusion des sous-titres et la sélection de playlists
- Mode générique (Generic Mode) optionnel pour les sites pris en charge par yt-dlp au-delà de YouTube
- Support multiplateforme et installation facile
<a id="features"></a>
## ✨ Fonctionnalités
<div align="center">
| Fonctionnalités de base | Fonctionnalités avancées | Fonctionnalités supplémentaires |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Tableau des formats | 🚫 Intégration de SponsorBlock | 🎞️ Affichage FPS/HDR |
| 🎵 Extraction audio | 📝 Sélection et fusion de sous-titres | 🔄 Mise à jour automatique de yt-dlp |
| ✨ Interface utilisateur simple | 💾 Enregistrement de la description et de la miniature | 🛠️ Détection de FFmpeg/yt-dlp/Deno |
| 📋 Support et sélecteur de playlists | 🚀 Limiteur de vitesse | ⚙️ Commandes personnalisées |
| 📑 Intégration des chapitres | ✂️ Découpage de sections vidéo | 🍪 Connexion avec Cookies |
| 📜 Historique des téléchargements | 🔄 Sélection du canal de version | 🌐 Support Proxy |
| 🎚️ Conversion du format audio | 🎬 Paramètres de format vidéo | 🆙 Onglet de mise à jour intégré |
| 🌍 Mode générique | 🔊 Normalisation audio (EBU R128) | 🌍 Localisation en 14 langues |
| 💾 Exportation de playlists | ⚙️ Qualité et sous-titres par défaut | |
</div>
<a id="installation"></a>
## 🚀 Installation
### ⚡ Installation rapide (Recommandé)
Installez YTSage via PyPI :
```bash
pip install ytsage
```
<details>
<summary>🔄 Mettre à jour une installation existante</summary>
```bash
pip install --upgrade ytsage
```
</details>
Lancez ensuite l'application :
```bash
ytsage
```
### 📦 Exécutables pré-construits
> [👉 Télécharger la dernière version](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Format | Description |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Installateur standard |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Avec FFmpeg inclus |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Version portable, aucune installation requise |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable avec FFmpeg, zippé |
<details>
<summary>🛠️ Étapes d'installation</summary>
1. **Installateur EXE (`.exe`)** : Double-cliquez sur le fichier et suivez l'assistant de configuration.
2. **Version portable (`.zip`)** : Extrayez l'archive vers l'emplacement souhaité et lancez `ytsage.exe`.
3. **FFmpeg inclus** : Choisissez les versions avec FFmpeg inclus si vous n'avez pas FFmpeg installé sur votre système.
</details>
#### 🐧 Linux
| Format | Description |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Paquet Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, portable |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Paquet RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Bundle |
<details>
<summary>🛠️ Étapes d'installation</summary>
- **DEB (`.deb`)** :
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Répare les dépendances manquantes si nécessaire
```
- **RPM (`.rpm`)** :
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)** :
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak** : Suivez les instructions sur Flathub ou lancez :
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Format | Description |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Application zippée pour Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Installateur d'image disque pour Apple Silicon |
<details>
<summary>🛠️ Étapes d'installation</summary>
- **Installateur DMG (`.dmg`)** : Double-cliquez pour monter, puis faites glisser `YTSage.app` dans votre dossier Applications.
- **Archive d'application (`.zip`)** : Extrayez le zip et déplacez `YTSage.app` dans votre dossier Applications.
*Note : Si vous rencontrez une erreur "L'application est endommagée", consultez la section de dépannage macOS ci-dessous.*
</details>
---
<details>
<summary>💻 Installation manuelle à partir des sources</summary>
### 1. Cloner le dépôt
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Installer les dépendances
#### ⚡ Avec uv
```bash
uv pip install .
```
#### 📦 Ou avec pip standard
```bash
pip install .
```
### 3. Lancer l'application
```bash
python -m ytsage.main
```
</details>
<a id="screenshots"></a>
## 📸 Captures d'écran
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Paramètres de téléchargement" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Téléchargement de playlist" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Paramètres de téléchargement</em></td>
<td align="center"><em>Téléchargement de playlist</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Sélection du format audio" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Options personnalisées" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Format audio</em></td>
<td align="center"><em>Options personnalisées</em></td>
</tr>
</table>
</div>
<a id="usage"></a>
## 📖 Utilisation
<details>
<summary>🎯 Utilisation de base</summary>
1. **Lancez YTSage**
2. **Collez l'URL YouTube** (ou utilisez le bouton "Paste URL")
3. **Cliquez sur "Analyze"**
4. **Sélectionnez le format :**
- `Video` pour les téléchargements vidéo
- `Audio Only` pour l'extraction audio
5. **Choisissez les options :**
- Activer les sous-titres et sélectionner la langue
- Activer la fusion des sous-titres
- Enregistrer la miniature
- Supprimer les segments sponsorisés
- Enregistrer la description
- Intégrer les chapitres
6. **Sélectionnez le répertoire de sortie**
7. **Cliquez sur "Download"**
> 💡 Le répertoire de téléchargement par défaut est le dossier "Téléchargements" de l'utilisateur.
</details>
<details>
<summary>📋 Téléchargement de playlist</summary>
1. **Collez l'URL de la playlist**
2. **Cliquez sur "Analyze"**
3. **Sélectionnez les vidéos du sélecteur de playlist (optionnel, toutes par défaut)**
4. **Choisissez le format/la qualité souhaitée**
5. **Cliquez sur "Download"**
> 💡 L'application gère automatiquement la file d'attente de téléchargement, et vous pouvez exporter les entrées de la playlist au format `.txt`, `.csv`, `.m3u` ou `.json`.
</details>
<details>
<summary>🌍 Mode générique pour les sites non-YouTube</summary>
Utilisez le mode générique (Generic Mode) lorsque vous souhaitez que YTSage accepte des URL de sites pris en charge par yt-dlp, tels que Dailymotion, CBC Gem, TikTok, et d'autres.
Comment l'utiliser :
1. Ouvrez `Download Settings`.
2. Activez `Generic Mode`.
3. Collez une URL de vidéo ou de playlist prise en charge qui n'est pas YouTube.
4. Cliquez sur `Analyze`.
5. Choisissez un format et téléchargez comme d'habitude.
Notes :
- Le mode générique ne modifie que la validation de l'URL à l'intérieur de YTSage. Le site cible doit toujours être pris en charge par votre version installée de yt-dlp.
- Certains sites nécessitent des cookies, une session de connexion, un proxy ou des arguments yt-dlp supplémentaires selon l'extracteur.
- Si un site échoue, mettez d'abord à jour yt-dlp depuis l'onglet de mise à jour intégré avant de signaler le problème.
</details>
<details>
<summary>🧰 Options média et de téléchargement</summary>
- **Options de sous-titres :** Filtrer les langues et intégrer les sous-titres dans le fichier vidéo.
- **Fusion de sous-titres :** Fusionner les sous-titres dans le fichier vidéo pour des sous-titres incrustés (hardcoded).
- **Enregistrer la description :** Enregistrer la description de la vidéo sous forme de fichier texte.
- **Enregistrer la miniature :** Enregistrer la miniature de la vidéo sous forme de fichier image.
- **Intégrer les chapitres :** Intégrer les marqueurs de chapitres comme métadonnées pour les lecteurs vidéo compatibles.
- **Supprimer les segments sponsorisés :** Supprimer les segments sponsorisés de la vidéo à l'aide de SponsorBlock.
- **Découper la vidéo :** Téléchargez uniquement des parties spécifiques d'une vidéo en spécifiant des plages temporelles au format `HH:MM:SS`.
</details>
<details>
<summary>⚙️ Paramètres de sortie et de fichier</summary>
- **Limiteur de vitesse :** Limiter la vitesse de téléchargement, par exemple `500K` pour 500 Ko/s.
- **Enregistrer le chemin de téléchargement :** Enregistre le chemin de téléchargement par défaut pour les futurs téléchargements. Disponible dans **Download Settings → Download Path**.
- **Résolution vidéo par défaut :** Définissez votre résolution vidéo préférée par défaut pour la sélection automatique (ex : 1080p, 720p). Disponible dans **Download Settings → Default Video Resolution**.
- **Langues de sous-titres par défaut :** Définissez les langues de sous-titres par défaut pour une sélection automatique (séparées par des virgules, ex : `fr,en`). Disponible dans **Download Settings → Default Subtitle Languages**.
- **Format du nom de fichier de sortie :** Personnalisez le format du nom de fichier de sortie à l'aide de variables telles que `%(title)s`, `%(uploader)s`, `%(playlist_index)s` et `%(resolution)s`. Disponible dans **Download Settings → Filename Format**.
- **Forcer le format de sortie :** Forcer les téléchargements vidéo dans un format de conteneur spécifique tel que `mp4`, `webm` ou `mkv`. Disponible dans **Download Settings → Output Format Settings**.
- **Conversion du format audio :** Convertir les téléchargements audio uniquement dans les formats préférés tels que `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis` ou `Best`. Disponible dans **Download Settings → Audio Format Settings**.
- **Normalisation audio :** Standardiser le volume pour les téléchargements audio uniquement à l'aide de l'EBU R128.
- **Connexions simultanées :** Augmentez considérablement la vitesse de téléchargement en téléchargeant les fichiers en plusieurs fragments simultanément. Disponible dans **Download Settings → General → Concurrent Connections** (1 par défaut, le maximum recommandé est de 8 à 10 pour éviter la limitation par IP).
</details>
<details>
<summary>🌐 Accès et réseau</summary>
- **Connexion avec cookies :** Connectez-vous à YouTube à l'aide de cookies pour accéder au contenu privé.
Comment l'utiliser :
1. **Recommandé :** Utilisez l'option intégrée `Extract cookies from browser` dans l'application, puis sélectionnez votre navigateur et éventuellement un profil.
2. Alternativement, extrayez les cookies manuellement :
a. Exportez les cookies de votre navigateur à l'aide d'une extension comme [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Copiez les cookies au format Netscape
c. Créez un fichier nommé `cookies.txt` et collez-y les cookies
d. Sélectionnez le fichier `cookies.txt` dans l'application
- **Support Proxy :** Utilisez un serveur proxy pour les téléchargements, par exemple `http://<proxy-server>:<port>`
- **Mode générique :** Permet à YTSage d'analyser et de télécharger à partir de sites non-YouTube pris en charge par yt-dlp. Activez-le depuis **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Outils et maintenance</summary>
- **Commandes personnalisées :** Accédez aux fonctionnalités avancées de yt-dlp via des arguments de ligne de commande.
- **Onglet de mise à jour :** Gérez les outils de mise à jour intégrés depuis un seul endroit dans Options personnalisées :
- **Mises à jour de yt-dlp :** Vérifiez les mises à jour et basculez entre les canaux de version Stable et Nightly.
- **Vérificateur de version FFmpeg :** Vérifiez votre version de FFmpeg et ouvrez les guides d'installation.
- **Mises à jour de Deno :** Vérifiez et mettez à jour le moteur d'exécution Deno.
- **Détection de FFmpeg/yt-dlp/Deno :** Détecte automatiquement les chemins et les versions de FFmpeg, yt-dlp et Deno à partir de la boîte de dialogue À propos.
- **Historique des téléchargements :** Affichez les téléchargements passés avec les miniatures et les statuts depuis le bouton **History**.
</details>
<details>
<summary>🌍 Localisation</summary>
YTSage prend en charge **14 langues** pour une accessibilité mondiale. Sélectionnez votre langue préférée dans **Custom Options → Language**.
### Langues prises en charge
| Langue | Code | Langue | Code |
|----------|------|----------|------|
| 🇺🇸 Anglais | `en` | 🇪🇸 Espagnol | `es` |
| 🇸🇦 Arabe | `ar` | 🇫🇷 Français | `fr` |
| 🇩🇪 Allemand | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonésien | `id` | 🇮🇹 Italien | `it` |
| 🇯🇵 Japonais | `ja` | 🇵🇱 Polonais | `pl` |
| 🇧🇷 Portugais | `pt` | 🇷🇺 Russe | `ru` |
| 🇹🇷 Turc | `tr` | 🇨🇳 Chinois | `zh` |
### Traductions du README
| Langue | Fichier | Langue | Fichier |
|----------|------|----------|------|
| 🇺🇸 Anglais | [README.md](../README.md) | 🇪🇸 Espagnol | [README.es.md](README.es.md) |
| 🇸🇦 Arabe | [README.ar.md](README.ar.md) | 🇫🇷 Français | [README.fr.md](README.fr.md) |
| 🇩🇪 Allemand | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonésien | [README.id.md](README.id.md) | 🇮🇹 Italien | [README.it.md](README.it.md) |
| 🇯🇵 Japonais | [README.ja.md](README.ja.md) | 🇵🇱 Polonais | [README.pl.md](README.pl.md) |
| 🇧🇷 Portugais | [README.pt.md](README.pt.md) | 🇷🇺 Russe | [README.ru.md](README.ru.md) |
| 🇹🇷 Turc | [README.tr.md](README.tr.md) | 🇨🇳 Chinois | [README.zh.md](README.zh.md) |
> 💡 **Vous souhaitez contribuer à une traduction ?** Consultez la section [Contribution](#contributing) pour nous aider à ajouter d'autres langues !
</details>
<a id="troubleshooting"></a>
## 🛠️ Dépannage
<details>
<summary>Cliquez pour voir les problèmes courants et les solutions</summary>
- **Le tableau des formats ne s'affiche pas :** Mettez à jour yt-dlp à la dernière version et passez à yt-dlp nightly.
- **Échec du téléchargement :** Vérifiez votre connexion Internet et assurez-vous que la vidéo est disponible.
- **Erreurs de téléchargement spécifiques :**
- **Vidéos privées :** Utilisez l'authentification par cookies pour accéder au contenu privé.
- **Contenu soumis à une limite d'âge :** Connectez-vous à votre compte YouTube pour visionner les vidéos avec limite d'âge.
- **Vidéos géo-bloquées :** Envisagez d'utiliser un VPN pour contourner les restrictions régionales.
- **Vidéos supprimées :** La vidéo n'est plus disponible sur YouTube.
- **Directs (Live streams) :** Les directs ne peuvent pas être téléchargés ; attendez la fin de la diffusion.
- **Erreurs réseau :** Vérifiez votre connexion Internet et réessayez.
- **URL non valides :** Assurez-vous que l'URL est correcte et provient d'une plateforme prise en charge.
- **Contenu Premium :** Nécessite un abonnement YouTube Premium.
- **Blocages pour droits d'auteur :** Le contenu est bloqué en raison de restrictions de droits d'auteur.
- **Fichiers vidéo et audio séparés après le téléchargement :** Cela se produit lorsque FFmpeg est manquant ou non détecté. YTSage nécessite FFmpeg pour fusionner les flux vidéo et audio de haute qualité.
- **Solution :** Assurez-vous que FFmpeg est installé et accessible dans le PATH de votre système. Pour les utilisateurs Windows, l'option la plus simple est de télécharger le fichier `YTSage-v<version>-ffmpeg.exe`, qui est livré avec FFmpeg.
---
#### 🛡️ Avertissement Windows Defender / Antivirus
Certains logiciels antivirus peuvent signaler les fichiers `.exe` comme de faux positifs. Il s'agit d'une **limitation connue** des applications packagées.
**Pourquoi cela se produit :**
- L'heuristique des antivirus peut identifier par erreur les exécutables packagés comme suspects.
**Alternatives sûres :**
- ✅ **Utilisez l'installation pip :** `pip install ytsage` (recommandé)
- ✅ **Compiler à partir des sources** : en suivant ce [guide](../.github/CI_CD_README.md)
- ✅ **Mettre l'application en liste blanche** dans votre logiciel antivirus.
#### 🍎 macOS : "L'application est endommagée et ne peut pas être ouverte"
Si vous voyez cette erreur sur macOS Sonoma ou une version plus récente, vous devez supprimer l'attribut de quarantaine.
1. **Ouvrez le Terminal** (vous pouvez le trouver en utilisant Spotlight).
2. **Tapez la commande suivante** mais **n'appuyez pas** encore sur Entrée. Assurez-vous d'inclure l'espace à la fin :
```bash
xattr -d com.apple.quarantine
```
3. **Faites glisser le fichier `YTSage.app`** depuis votre fenêtre Finder et déposez-le directement dans la fenêtre du Terminal. Cela collera automatiquement le chemin correct du fichier.
4. **Appuyez sur Entrée** pour exécuter la commande.
5. **Essayez d'ouvrir à nouveau YTSage.app.** Il devrait maintenant se lancer correctement.
---
#### **Emplacements de configuration (Avancé)**
- **Windows :** `%LOCALAPPDATA%\YTSage`
- **macOS :** `~/Library/Application Support/YTSage`
- **Linux :** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Sponsor
Si YTSage vous fait gagner du temps, envisagez de sponsoriser le projet. Le parrainage aide à couvrir le temps de développement, les tests sur toutes les plateformes et les améliorations futures.
- GitHub Sponsors : https://github.com/sponsors/oop7
- Le lien de parrainage est également disponible directement dans l'application via la boîte de dialogue À propos.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contributing"></a>
## 👥 Contribution
Nous accueillons les contributions avec plaisir ! Voici comment vous pouvez aider :
1. 🍴 Forkez le dépôt
2. 🌿 Créez votre branche de fonctionnalité :
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 Committez vos modifications :
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 Pushez vers la branche :
```bash
git push origin feature/AmazingFeature
```
5. 🔄 Ouvrez une Pull Request
### 🌍 Contribuer aux traductions
- Mettez à jour le fichier README localisé correspondant (par exemple `readme-translations/README.fr.md`)
- Gardez les chaînes de l'application synchronisées en éditant `ytsage/languages/<code>.json`
- Si votre langue est manquante, commencez par `README.md` et créez `README.<code>.md`
<details>
<summary>📂 Structure du projet</summary>
## YTSage - Structure du projet
Ce document décrit la structure organisée des dossiers de YTSage.
### 📁 Structure du projet
```
YTSage/
├── 📁 .github/ # Configuration GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Modèles de tickets
│ │ └── 🐛-bug-report.md # Modèle de rapport de bug
│ ├─── 📁 workflows/ # Workflows GitHub Actions
│ │ ├── build-linux.yml # Workflow de build Linux
│ │ ├── build-macos.yml # Workflow de build macOS
│ │ │── build-windows.yml # Workflow de build Windows
| | └── release-all.yml # Workflow de release master
│ └── 📄 CI_CD_README.md # Documentation CI/CD
├── 📁 branding/ # Actifs de marque (Captures d'écran, SVGs)
│ ├── 📁 icons/ # Icônes de l'application
│ ├── 📁 screenshots/ # Captures d'écran pour la documentation
│ └── 📁 svg/ # Actifs SVG
├── 📄 LICENSE # Fichier de licence
├── 📄 pyproject.toml # Métadonnées du projet et dépendances
├── 📄 README.md # Documentation du projet
├── 📄 requirements.txt # Dépendances Python (dev)
└── 📁 ytsage/ # Paquet source
├── 📁 assets/ # Actifs d'exécution
│ ├── 📁 Icon/ # Icônes de l'application
│ └── 📁 sound/ # Fichiers audio
├── 📁 languages/ # Fichiers de localisation
│ ├── 📄 ar.json # Traduction arabe
│ ├── 📄 de.json # Traduction allemande
│ ├── 📄 en.json # Traduction anglaise
│ └── ... # Autres langues
├── 📁 core/ # Logique métier principale
│ ├── 📄 __init__.py # Initialisation du paquet core
│ ├── 📄 ytsage_deno.py # Intégration Deno
│ ├── 📄 ytsage_downloader.py # Fonctionnalité de téléchargement
│ ├── 📄 ytsage_ffmpeg.py # Intégration FFmpeg
│ ├── 📄 ytsage_utils.py # Fonctions utilitaires
│ └── 📄 ytsage_yt_dlp.py # Intégration yt-dlp
├── 📁 gui/ # Composants de l'interface utilisateur
│ ├── 📄 __init__.py # Initialisation du paquet GUI
│ ├── 📄 ytsage_gui_main.py # Fenêtre principale de l'application
│ └── 📁 ytsage_gui_dialogs/ # Classes de dialogues
├── 📁 utils/ # Modules utilitaires
│ ├── 📄 __init__.py # Initialisation du paquet utils
│ ├── 📄 ytsage_config_manager.py # Gestion de la configuration
│ └── 📄 ytsage_logger.py # Utilitaires de log
├── 📄 __init__.py # Point d'entrée du paquet
└── 📄 main.py # Script d'exécution principal
```
</details>
## ⭐️ Historique des étoiles
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Licence
Ce projet est sous licence MIT - voir le fichier [LICENSE](../LICENSE) pour plus de détails.
## 🙏 Remerciements
<details>
<summary>Afficher les remerciements</summary>
<div align="center">
<p>Un grand merci à tous ceux qui ont contribué à ce projet en ouvrant un ticket pour suggérer une amélioration ou signaler un bug.</p>
<table>
<tr class="section"><th colspan="2">Composants de base</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Moteur de téléchargement</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Traitement média</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime pour l'intégration avec yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Bibliothèques et Frameworks</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>Framework GUI</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Traitement d'images</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>Requêtes HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Gestion des versions et des paquets</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Rendu Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td>
</tr>
<tr class="section"><th colspan="2">Actifs & Contributeurs</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>Son de notification</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Contributeur de code</td>
</tr>
</table>
</div>
</details>
## ⚠️ Clause de non-responsabilité
Cet outil est destiné à un usage personnel uniquement. Veuillez respecter les conditions d'utilisation de YouTube et les droits des créateurs de contenu.
---
<div align="center">
Fait avec ❤️ par [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**एक आधुनिक YouTube डाउनलोडर, एक स्वच्छ PySide6 इंटरफ़ेस के साथ।**
किसी भी गुणवत्ता में वीडियो डाउनलोड करें, ऑडियो निकालें, उपशीर्षक प्राप्त करें, और बहुत कुछ।
### 🌍 README भाषाएँ
अंग्रेज़ी: [EN](../README.md)
| अरबी: [AR](README.ar.md)
| जर्मन: [DE](README.de.md)
| स्पेनिश: [ES](README.es.md)
| फ्रेंच: [FR](README.fr.md)
| हिंदी: [HI](README.hi.md)
| इंडोनेशियाई: [ID](README.id.md)
| इतालवी: [IT](README.it.md)
| जापानी: [JA](README.ja.md)
| पोलिश: [PL](README.pl.md)
| पुर्तगाली: [PT](README.pt.md)
| रूसी: [RU](README.ru.md)
| तुर्की: [TR](README.tr.md)
| चीनी: [ZH](README.zh.md)
<p align="center">
<a href="#installation">स्थापना</a> •
<a href="#features">विशेषताएँ</a> •
<a href="#usage">उपयोग</a> •
<a href="#screenshots">स्क्रीनशॉट</a> •
<a href="#troubleshooting">समस्या निवारण</a> •
<a href="#sponsor">प्रायोजक</a> •
<a href="#contributing">योगदान</a>
</p>
</div>
---
<a id="why-ytsage"></a>
## ❓ YTSage क्यों?
YTSage उन उपयोगकर्ताओं के लिए डिज़ाइन किया गया है जो एक **सरल लेकिन शक्तिशाली YouTube डाउनलोडर** चाहते हैं। अन्य उपकरणों के विपरीत, यह प्रदान करता है:
- एक आधुनिक और स्वच्छ PySide6 इंटरफ़ेस
- वीडियो, ऑडियो और उपशीर्षक के लिए वन-क्लिक डाउनलोड
- SponsorBlock, उपशीर्षक मर्जिंग, और प्लेलिस्ट चयन जैसी उन्नत सुविधाएँ
- YouTube से इतर yt-dlp द्वारा समर्थित साइटों के लिए वैकल्पिक जेनेरिक मोड
- क्रॉस-प्लेटफ़ॉर्म समर्थन और आसान स्थापना
<a id="features"></a>
## ✨ विशेषताएँ
<div align="center">
| बुनियादी विशेषताएँ | उन्नत विशेषताएँ | अतिरिक्त विशेषताएँ |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 प्रारूप तालिका | 🚫 SponsorBlock एकीकरण | 🎞️ FPS/HDR डिस्प्ले |
| 🎵 ऑडियो निष्कर्षण | 📝 उपशीर्षक चयन और मर्जिंग | 🔄 ऑटो yt-dlp अपडेट |
| ✨ सरल यूजर इंटरफेस | 💾 विवरण और थंबनेल सहेजें | 🛠️ FFmpeg/yt-dlp/Deno डिटेक्शन |
| 📋 प्लेलिस्ट समर्थन और चयनकर्ता | 🚀 गति सीमित करने वाला | ⚙️ कस्टम कमांड |
| 📑 अध्याय एकीकरण | ✂️ वीडियो अनुभाग ट्रिम करें | 🍪 कुकी लॉगिन |
| 📜 डाउनलोड इतिहास | 🔄 रिलीज़ चैनल चयन | 🌐 प्रॉक्सी समर्थन |
| 🎚️ ऑडियो प्रारूप रूपांतरण | 🎬 वीडियो प्रारूप सेटिंग्स | 🆙 एकीकृत अपडेट टैब |
| 🌍 जेनेरिक मोड | 🔊 ऑडियो सामान्यीकरण (EBU R128) | 🌍 14 भाषाओं में स्थानीयकरण |
| 💾 प्लेलिस्ट निर्यात | ⚙️ डिफ़ॉल्ट गुणवत्ता और उपशीर्षक | |
</div>
<a id="installation"></a>
## 🚀 स्थापना
### ⚡ त्वरित स्थापना (अनुशंसित)
PyPI के माध्यम से YTSage स्थापित करें:
```bash
pip install ytsage
```
<details>
<summary>🔄 मौजूदा स्थापना को अपडेट करें</summary>
```bash
pip install --upgrade ytsage
```
</details>
फिर एप्लिकेशन चलाएँ:
```bash
ytsage
```
### 📦 प्री-बिल्ट एक्जीक्यूटेबल्स (एक्ज़ीक्यूटेबल्स)
> [👉 नवीनतम रिलीज़ डाउनलोड करें](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| प्रारूप | विवरण |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | मानक इंस्टॉलर |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | FFmpeg के साथ |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | पोर्टेबल संस्करण, स्थापना की आवश्यकता नहीं है |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | FFmpeg के साथ पोर्टेबल, ज़िप्ड |
<details>
<summary>🛠️ स्थापना चरण</summary>
1. **EXE इंस्टॉलर (`.exe`)**: फ़ाइल पर डबल-क्लिक करें और सेटअप विज़ार्ड का पालन करें।
2. **पोर्टेबल संस्करण (`.zip`)**: संग्रह को इच्छित स्थान पर निकालें और `ytsage.exe` चलाएँ।
3. **बिल्ट-इन FFmpeg**: यदि आपके सिस्टम पर FFmpeg स्थापित नहीं है, तो बिल्ट-इन FFmpeg वाले संस्करण चुनें।
</details>
#### 🐧 Linux
| प्रारूप | विवरण |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | डेबियन पैकेज |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, पोर्टेबल |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM पैकेज |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak बंडल |
<details>
<summary>🛠️ स्थापना चरण</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # यदि आवश्यक हो तो गायब निर्भरताओं को ठीक करें
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Flathub पर निर्देशों का पालन करें या चलाएँ:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| प्रारूप | विवरण |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon के लिए ज़िप्ड ऐप |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon के लिए डिस्क इमेज इंस्टॉलर |
<details>
<summary>🛠️ स्थापना चरण</summary>
- **DMG इंस्टॉलर (`.dmg`)**: माउंट करने के लिए डबल-क्लिक करें, फिर `YTSage.app` को अपने एप्लिकेशन फ़ोल्डर में खींचें।
- **ऐप आर्काइव (`.zip`)**: ज़िप निकालें और `YTSage.app` को अपने एप्लिकेशन फ़ोल्डर में ले जाएँ।
*नोट: यदि आपको "ऐप क्षतिग्रस्त है" त्रुटि मिलती है, तो नीचे macOS समस्या निवारण अनुभाग देखें।*
</details>
---
<details>
<summary>💻 स्रोत से मैन्युअल स्थापना</summary>
### 1. रिपॉजिटरी क्लोन करें
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. निर्भरताएँ स्थापित करें
#### ⚡ uv के साथ
```bash
uv pip install .
```
#### 📦 या मानक pip के साथ
```bash
pip install .
```
### 3. एप्लिकेशन चलाएँ
```bash
python -m ytsage.main
```
</details>
<a id="screenshots"></a>
## 📸 स्क्रीनशॉट
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="डाउनलोड सेटिंग्स" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="प्लेलिस्ट डाउनलोड" width="400"/></td>
</tr>
<tr>
<td align="center"><em>डाउनलोड सेटिंग्स</em></td>
<td align="center"><em>प्लेलिस्ट डाउनलोड</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="ऑडियो प्रारूप चयन" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="कस्टम विकल्प" width="400"/></td>
</tr>
<tr>
<td align="center"><em>ऑडियो प्रारूप</em></td>
<td align="center"><em>कस्टम विकल्प</em></td>
</tr>
</table>
</div>
<a id="usage"></a>
## 📖 उपयोग
<details>
<summary>🎯 बुनियादी उपयोग</summary>
1. **YTSage लॉन्च करें**
2. **YouTube URL पेस्ट करें** (या "Paste URL" बटन का उपयोग करें)
3. **"Analyze" पर क्लिक करें**
4. **प्रारूप चुनें:**
- वीडियो डाउनलोड के लिए `Video`
- ऑडियो निष्कर्षण के लिए `Audio Only`
5. **विकल्प चुनें:**
- उपशीर्षक सक्षम करें और भाषा चुनें
- उपशीर्षक मर्जिंग सक्षम करें
- थंबनेल सहेजें
- प्रायोजित अनुभाग हटाएँ
- विवरण सहेजें
- अध्याय एम्बेड करें
6. **आउटपुट निर्देशिका चुनें**
7. **"Download" पर क्लिक करें**
> 💡 डिफ़ॉल्ट डाउनलोड निर्देशिका उपयोगकर्ता का "Downloads" फ़ोल्डर है।
</details>
<details>
<summary>📋 प्लेलिस्ट डाउनलोड</summary>
1. **प्लेलिस्ट URL पेस्ट करें**
2. **"Analyze" पर क्लिक करें**
3. **प्लेलिस्ट चयनकर्ता से वीडियो चुनें (वैकल्पिक, डिफ़ॉल्ट रूप से सभी)**
4. **वांछित प्रारूप/गुणवत्ता चुनें**
5. **"Download" पर क्लिक करें**
> 💡 एप्लिकेशन डाउनलोड कतार को स्वचालित रूप से प्रबंधित करता है, और आप प्लेलिस्ट प्रविष्टियों को `.txt`, `.csv`, `.m3u`, या `.json` फ़ाइलों के रूप में निर्यात कर सकते हैं।
</details>
<details>
<summary>🌍 गैर-YouTube साइटों के लिए जेनेरिक मोड</summary>
जेनेरिक मोड (Generic Mode) का उपयोग तब करें जब आप चाहते हैं कि YTSage yt-dlp द्वारा समर्थित साइटों जैसे Dailymotion, CBC Gem, TikTok और अन्य से URL स्वीकार करे।
इसका उपयोग कैसे करें:
1. `Download Settings` खोलें।
2. `Generic Mode` सक्षम करें।
3. एक समर्थित वीडियो या प्लेलिस्ट URL पेस्ट करें जो YouTube नहीं है।
4. `Analyze` पर क्लिक करें।
5. एक प्रारूप चुनें और हमेशा की तरह डाउनलोड करें।
नोट्स:
- जेनेरिक मोड केवल YTSage के अंदर URL सत्यापन को बदलता है। लक्ष्य साइट अभी भी आपके स्थापित yt-dlp संस्करण द्वारा समर्थित होनी चाहिए।
- कुछ साइटों को एक्सट्रैक्टर के आधार पर कुकीज़, लॉगिन, प्रॉक्सी या अतिरिक्त yt-dlp तर्कों की आवश्यकता होती है।
- यदि कोई साइट विफल हो जाती है, तो समस्या की रिपोर्ट करने से पहले एकीकृत अपडेट टैब से yt-dlp को अपडेट करें।
</details>
<details>
<summary>🧰 मीडिया और डाउनलोड विकल्प</summary>
- **उपशीर्षक विकल्प:** भाषाओं को फ़िल्टर करें और उपशीर्षक को वीडियो फ़ाइल में एम्बेड करें।
- **उपशीर्षक मर्जिंग:** हार्डकोडेड उपशीर्षक के लिए वीडियो फ़ाइल में उपशीर्षक मर्ज करें।
- **विवरण सहेजें:** वीडियो विवरण को टेक्स्ट फ़ाइल के रूप में सहेजें।
- **थंबनेल सहेजें:** वीडियो थंबनेल को छवि फ़ाइल के रूप में सहेजें।
- **अध्याय एम्बेड करें:** संगत वीडियो प्लेयर के लिए मेटाडेटा के रूप में अध्याय मार्कर शामिल करें।
- **प्रायोजित अनुभाग हटाएँ:** SponsorBlock का उपयोग करके वीडियो से प्रायोजित अनुभाग हटाएँ।
- **वीडियो ट्रिम करें:** `HH:MM:SS` प्रारूप में समय सीमा निर्दिष्ट करके वीडियो के केवल विशिष्ट भागों को डाउनलोड करें।
</details>
<details>
<summary>⚙️ आउटपुट और फ़ाइल सेटिंग्स</summary>
- **गति सीमित करने वाला:** डाउनलोड गति को सीमित करें, उदाहरण के लिए `500K` 500 KB/s के लिए।
- **डाउनलोड पथ सहेजें:** भविष्य के डाउनलोड के लिए डिफ़ॉल्ट डाउनलोड पथ सहेजता है। **Download Settings → Download Path** में उपलब्ध है।
- **डिफ़ॉल्ट वीडियो रिज़ॉल्यूशन:** स्वचालित चयन के लिए अपना पसंदीदा वीडियो रिज़ॉल्यूशन सेट करें (जैसे 1080p, 720p)। **Download Settings → Default Video Resolution** में उपलब्ध है।
- **डिफ़ॉल्ट उपशीर्षक भाषाएँ:** स्वचालित चयन के लिए डिफ़ॉल्ट उपशीर्षक भाषाएँ सेट करें (अल्पविराम से अलग, जैसे `hi,en`)। **Download Settings → Default Subtitle Languages** में उपलब्ध है।
- **फ़ाइल नाम प्रारूप:** `%(title)s`, `%(uploader)s`, `%(playlist_index)s` और `%(resolution)s` जैसे चरों का उपयोग करके आउटपुट फ़ाइल नाम प्रारूप को अनुकूलित करें। **Download Settings → Filename Format** में उपलब्ध है।
- **आउटपुट प्रारूप बाध्य करें:** वीडियो डाउनलोड को `mp4`, `webm` या `mkv` जैसे विशिष्ट कंटेनर प्रारूप में बाध्य करें। **Download Settings → Output Format Settings** में उपलब्ध है।
- **ऑडियो प्रारूप रूपांतरण:** केवल ऑडियो डाउनलोड को `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, या `Best` जैसे पसंदीदा प्रारूपों में परिवर्तित करें। **Download Settings → Audio Format Settings** में उपलब्ध है।
- **ऑडियो सामान्यीकरण:** EBU R128 का उपयोग करके केवल ऑडियो डाउनलोड के लिए वॉल्यूम को मानकीकृत करें।
- **एक साथ कनेक्शन:** एक साथ कई टुकड़ों में फ़ाइलें डाउनलोड करके डाउनलोड गति को काफी बढ़ाएँ। **Download Settings → General → Concurrent Connections** (डिफ़ॉल्ट 1, IP ब्लॉक से बचने के लिए अधिकतम 8-10 अनुशंसित) में उपलब्ध है।
</details>
<details>
<summary>🌐 एक्सेस और नेटवर्क</summary>
- **कुकी लॉगिन:** निजी सामग्री तक पहुँचने के लिए कुकीज़ का उपयोग करके YouTube में लॉगिन करें।
उपयोग:
1. **अनुशंसित:** ऐप में बिल्ट-इन `Extract cookies from browser` विकल्प का उपयोग करें, फिर अपना ब्राउज़र और वैकल्पिक रूप से प्रोफ़ाइल चुनें।
2. वैकल्पिक रूप से, कुकीज़ मैन्युअल रूप से निकालें:
a. [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file) जैसे एक्सटेंशन का उपयोग करके अपने ब्राउज़र से कुकीज़ निर्यात करें
b. नेटस्केप प्रारूप में कुकीज़ कॉपी करें
c. `cookies.txt` नामक फ़ाइल बनाएँ और कुकीज़ पेस्ट करें
d. ऐप में `cookies.txt` फ़ाइल चुनें
- **प्रॉक्सी समर्थन:** डाउनलोड के लिए प्रॉक्सी सर्वर का उपयोग करें, जैसे `http://<proxy-server>:<port>`
- **जेनेरिक मोड:** YTSage को yt-dlp द्वारा समर्थित गैर-YouTube साइटों से विश्लेषण और डाउनलोड करने की अनुमति देता है। इसे **Download Settings → Generic Mode** से सक्षम करें।
</details>
<details>
<summary>🛠️ उपकरण और रखरखाव</summary>
- **कस्टम कमांड:** कमांड-लाइन तर्कों के माध्यम से उन्नत yt-dlp सुविधाओं तक पहुँचें।
- **अपडेट टैब:** कस्टम विकल्पों में एक ही स्थान से बिल्ट-इन अपडेट टूल प्रबंधित करें:
- **yt-dlp अपडेट:** अपडेट के लिए जाँच करें और स्टेबल और नाइटली रिलीज़ चैनलों के बीच स्विच करें।
- **FFmpeg संस्करण जाँचकर्ता:** अपने FFmpeg संस्करण को सत्यापित करें और स्थापना मार्गदर्शिकाएँ खोलें।
- **Deno अपडेट:** Deno रनटाइम की जाँच करें और अपडेट करें।
- **FFmpeg/yt-dlp/Deno डिटेक्शन:** अबाउट डायलॉग से FFmpeg, yt-dlp और Deno के पथ और संस्करण का स्वचालित रूप से पता लगाता है।
- **डाउनलोड इतिहास:** **History** बटन से थंबनेल और स्थितियों के साथ पिछले डाउनलोड देखें।
</details>
<details>
<summary>🌍 स्थानीयकरण</summary>
YTSage वैश्विक पहुँच के लिए **14 भाषाओं** का समर्थन करता है। **Custom Options → Language** में अपनी पसंदीदा भाषा चुनें।
### समर्थित भाषाएँ
| भाषा | कोड | भाषा | कोड |
|----------|------|----------|------|
| 🇺🇸 अंग्रेज़ी | `en` | 🇪🇸 स्पेनिश | `es` |
| 🇸🇦 अरबी | `ar` | 🇫🇷 फ्रेंच | `fr` |
| 🇩🇪 जर्मन | `de` | 🇮🇳 हिंदी | `hi` |
| 🇮🇩 इंडोनेशियाई | `id` | 🇮🇹 इतालवी | `it` |
| 🇯🇵 जापानी | `ja` | 🇵🇱 पोलिश | `pl` |
| 🇧🇷 पुर्तगाली | `pt` | 🇷🇺 रूसी | `ru` |
| 🇹🇷 तुर्की | `tr` | 🇨🇳 चीनी | `zh` |
### README अनुवाद
| भाषा | फ़ाइल | भाषा | फ़ाइल |
|----------|------|----------|------|
| 🇺🇸 अंग्रेज़ी | [README.md](../README.md) | 🇪🇸 स्पेनिश | [README.es.md](README.es.md) |
| 🇸🇦 अरबी | [README.ar.md](README.ar.md) | 🇫🇷 फ्रेंच | [README.fr.md](README.fr.md) |
| 🇩🇪 जर्मन | [README.de.md](README.de.md) | 🇮🇳 हिंदी | [README.hi.md](README.hi.md) |
| 🇮🇩 इंडोनेशियाई | [README.id.md](README.id.md) | 🇮🇹 इतालवी | [README.it.md](README.it.md) |
| 🇯🇵 जापानी | [README.ja.md](README.ja.md) | 🇵🇱 पोलिश | [README.pl.md](README.pl.md) |
| 🇧🇷 पुर्तगाली | [README.pt.md](README.pt.md) | 🇷🇺 रूसी | [README.ru.md](README.ru.md) |
| 🇹🇷 तुर्की | [README.tr.md](README.tr.md) | 🇨🇳 चीनी | [README.zh.md](README.zh.md) |
> 💡 **अनुवाद में मदद करना चाहते हैं?** अधिक भाषाओं को जोड़ने में हमारी सहायता करने के लिए [योगदान](#contributing) अनुभाग देखें!
</details>
<a id="troubleshooting"></a>
## 🛠️ समस्या निवारण
<details>
<summary>सामान्य समस्याओं और समाधानों को देखने के लिए क्लिक करें</summary>
- **प्रारूप तालिका दिखाई नहीं दे रही है:** yt-dlp को नवीनतम संस्करण में अपडेट करें और yt-dlp नाइटली पर स्विच करें।
- **डाउनलोड विफल:** अपना इंटरनेट कनेक्शन जाँचें और सुनिश्चित करें कि वीडियो उपलब्ध है।
- **विशिष्ट डाउनलोड त्रुटियाँ:**
- **निजी वीडियो:** निजी सामग्री तक पहुँचने के लिए कुकी प्रमाणीकरण का उपयोग करें।
- **आयु-प्रतिबंधित सामग्री:** आयु-प्रतिबंधित वीडियो देखने के लिए अपने YouTube खाते में लॉग इन करें।
- **जियो-ब्लॉक किए गए वीडियो:** क्षेत्रीय प्रतिबंधों को दरकिनार करने के लिए VPN का उपयोग करने पर विचार करें।
- **हटाए गए वीडियो:** वीडियो अब YouTube पर उपलब्ध नहीं है।
- **लाइव स्ट्रीम:** प्रसारण के दौरान लाइव स्ट्रीम डाउनलोड नहीं की जा सकतीं; स्ट्रीम समाप्त होने तक प्रतीक्षा करें।
- **नेटवर्क त्रुटियाँ:** अपना इंटरनेट कनेक्शन जाँचें और पुनः प्रयास करें।
- **अमान्य URL:** सुनिश्चित करें कि URL सही है और समर्थित प्लेटफ़ॉर्म से है।
- **प्रीमियम सामग्री:** YouTube प्रीमियम सदस्यता की आवश्यकता है।
- **कॉपीराइट ब्लॉक:** कॉपीराइट प्रतिबंधों के कारण सामग्री अवरुद्ध है।
- **डाउनलोड के बाद वीडियो और ऑडियो फ़ाइलें अलग हैं:** यह तब होता है जब FFmpeg गायब होता है या पता नहीं चल पाता है। उच्च गुणवत्ता वाले वीडियो और ऑडियो स्ट्रीम को मर्ज करने के लिए YTSage को FFmpeg की आवश्यकता होती है।
- **समाधान:** सुनिश्चित करें कि FFmpeg स्थापित है और आपके सिस्टम PATH में सुलभ है। विंडोज उपयोगकर्ताओं के लिए, सबसे आसान विकल्प `YTSage-v<version>-ffmpeg.exe` फ़ाइल डाउनलोड करना है, जो FFmpeg के साथ आती है।
---
#### 🛡️ Windows Defender / एंटीवायरस चेतावनी
कुछ एंटीवायरस सॉफ़्टवेयर `.exe` फ़ाइलों को गलत सकारात्मक (फॉल्स पॉजिटिव) के रूप में चिह्नित कर सकते हैं। यह पैक किए गए एप्लिकेशन की **ज्ञात सीमा** है।
**यह क्यों होता है:**
- एंटीवायरस हिउरिस्टिक्स पैक किए गए एक्जीक्यूटेबल्स को संदिग्ध के रूप में गलत तरीके से पहचान सकते हैं।
**सुरक्षित विकल्प:**
- ✅ **pip स्थापना का उपयोग करें:** `pip install ytsage` (अनुशंसित)
- ✅ **स्रोत से बिल्ड करें**: इस [गाइड](../.github/CI_CD_README.md) का पालन करते हुए
- ✅ **एप्लिकेशन को व्हाइटलिस्ट करें** अपने एंटीवायरस सॉफ़्टवेयर में।
#### 🍎 macOS: "ऐप क्षतिग्रस्त है और खोला नहीं जा सकता"
यदि आप macOS Sonoma या नए पर यह त्रुटि देखते हैं, तो आपको क्वारंटाइन एट्रिब्यूट को हटाने की आवश्यकता है।
1. **टर्मिनल खोलें** (आप इसे Spotlight का उपयोग करके पा सकते हैं)।
2. **निम्न कमांड टाइप करें** लेकिन अभी Enter **न दबाएँ**। सुनिश्चित करें कि अंत में स्थान शामिल है:
```bash
xattr -d com.apple.quarantine
```
3. **अपनी फाइंडर विंडो से `YTSage.app` फ़ाइल खींचें** और इसे सीधे टर्मिनल विंडो में छोड़ें। यह स्वचालित रूप से सही फ़ाइल पथ पेस्ट कर देगा।
4. **Enter दबाएँ** कमांड चलाने के लिए।
5. **YTSage.app को फिर से खोलने का प्रयास करें।** इसे अब ठीक से लॉन्च होना चाहिए।
---
#### **कॉन्फ़िगरेशन स्थान (उन्नत)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 प्रायोजक
यदि YTSage आपका समय बचाता है, तो कृपया प्रोजेक्ट को प्रायोजित करने पर विचार करें। प्रायोजन विकास समय, सभी प्लेटफ़ॉर्म पर परीक्षण और भविष्य के सुधारों को कवर करने में मदद करता है।
- GitHub Sponsors: https://github.com/sponsors/oop7
- प्रायोजन लिंक ऐप में अबाउट डायलॉग के माध्यम से सीधे उपलब्ध है।
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contributing"></a>
## 👥 योगदान
हम योगदान का स्वागत करते हैं! यहाँ बताया गया है कि आप कैसे मदद कर सकते हैं:
1. 🍴 रिपॉजिटरी को फोर्क करें
2. 🌿 अपनी फीचर शाखा बनाएँ:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 अपने बदलाव कमिट करें:
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 शाखा में पुश करें:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 एक पुल रिक्वेस्ट खोलें
### 🌍 अनुवाद में योगदान करें
- संबंधित स्थानीयकृत README फ़ाइल को अपडेट करें (जैसे `readme-translations/README.hi.md`)
- `ytsage/languages/<code>.json` को संपादित करके ऐप स्ट्रिंग्स को सिंक में रखें
- यदि आपकी भाषा गायब है, तो `README.md` से शुरू करें और `README.<code>.md` बनाएँ
<details>
<summary>📂 प्रोजेक्ट संरचना</summary>
## YTSage - प्रोजेक्ट संरचना
यह दस्तावेज़ YTSage की संगठित फ़ोल्डर संरचना का वर्णन करता है।
### 📁 प्रोजेक्ट संरचना
```
YTSage/
├── 📁 .github/ # GitHub कॉन्फ़िगरेशन
│ ├── 📁 ISSUE_TEMPLATE/ # इश्यू टेम्पलेट
│ │ └── 🐛-bug-report.md # बग रिपोर्ट टेम्पलेट
│ ├─── 📁 workflows/ # GitHub Actions वर्कफ़्लो
│ │ ├── build-linux.yml # लिनक्स बिल्ड वर्कफ़्लो
│ │ ├── build-macos.yml # macOS बिल्ड वर्कफ़्लो
│ │ │── build-windows.yml # विंडोज बिल्ड वर्कफ़्लो
| | └── release-all.yml # मास्टर रिलीज़ वर्कफ़्लो
│ └── 📄 CI_CD_README.md # CI/CD दस्तावेज़ीकरण
├── 📁 branding/ # ब्रांडिंग एसेट्स (स्क्रीनशॉट, SVGs)
│ ├── 📁 icons/ # ऐप आइकन
│ ├── 📁 screenshots/ # दस्तावेज़ीकरण के लिए स्क्रीनशॉट
│ └── 📁 svg/ # SVG एसेट्स
├── 📄 LICENSE # लाइसेंस फ़ाइल
├── 📄 pyproject.toml # प्रोजेक्ट मेटाडेटा और निर्भरताएँ
├── 📄 README.md # प्रोजेक्ट दस्तावेज़ीकरण
├── 📄 requirements.txt # पायथन निर्भरताएँ (dev)
└── 📁 ytsage/ # स्रोत कोड पैकेज
├── 📁 assets/ # रनटाइम एसेट्स
│ ├── 📁 Icon/ # ऐप आइकन
│ └── 📁 sound/ # ऑडियो फ़ाइलें
├── 📁 languages/ # स्थानीयकरण फ़ाइलें
│ ├── 📄 ar.json # अरबी अनुवाद
│ ├── 📄 de.json # जर्मन अनुवाद
│ ├── 📄 en.json # अंग्रेज़ी अनुवाद
│ └── ... # अन्य भाषाएँ
├── 📁 core/ # मुख्य व्यावसायिक तर्क
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Deno एकीकरण
│ ├── 📄 ytsage_downloader.py # डाउनलोड कार्यक्षमता
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg एकीकरण
│ ├── 📄 ytsage_utils.py # उपयोगिता कार्य
│ └── 📄 ytsage_yt_dlp.py # yt-dlp एकीकरण
├── 📁 gui/ # यूजर इंटरफेस घटक
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # ऐप की मुख्य विंडो
│ └── 📁 ytsage_gui_dialogs/ # संवाद वर्ग
├── 📁 utils/ # उपयोगिता मॉड्यूल
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # कॉन्फ़िगरेशन प्रबंधन
│ └── 📄 ytsage_logger.py # लॉगिंग टूल
├── 📄 __init__.py # पैकेज प्रविष्टि बिंदु
└── 📄 main.py # मुख्य निष्पादन स्क्रिप्ट
```
</details>
## ⭐️ स्टार इतिहास
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 लाइसेंस
यह प्रोजेक्ट MIT लाइसेंस के तहत है - विवरण के लिए [LICENSE](../LICENSE) फ़ाइल देखें।
## 🙏 धन्यवाद
<details>
<summary>धन्यवाद प्रदर्शित करें</summary>
<div align="center">
<p>उन सभी को बहुत-बहुत धन्यवाद जिन्होंने सुधार का सुझाव देने या बग की रिपोर्ट करने के लिए इश्यू खोलकर इस प्रोजेक्ट में योगदान दिया है।</p>
<table>
<tr class="section"><th colspan="2">मुख्य घटक</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>डाउनलोड इंजन</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>मीडिया प्रसंस्करण</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>yt-dlp एकीकरण के लिए रनटाइम</td>
</tr>
<tr class="section"><th colspan="2">पुस्तकालय और फ्रेमवर्क</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI फ्रेमवर्क</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>छवि प्रसंस्करण</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>HTTP अनुरोध</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>संस्करण प्रबंधन और पैकेजिंग</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown रेंडरिंग</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>लॉगिंग</td>
</tr>
<tr class="section"><th colspan="2">एसेट्स और योगदानकर्ता</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>अधिसूचना ध्वनि</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>कोड योगदानकर्ता</td>
</tr>
</table>
</div>
</details>
## ⚠️ अस्वीकरण
यह उपकरण केवल व्यक्तिगत उपयोग के लिए है। कृपया YouTube की सेवा की शर्तों और सामग्री रचनाकारों के अधिकारों का सम्मान करें।
---
<div align="center">
[oop7](https://github.com/oop7) द्वारा ❤️ के साथ बनाया गया
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Pengunduh YouTube modern dengan antarmuka PySide6 yang bersih.**
Unduh video dalam kualitas apa pun, ekstrak audio, dapatkan subtitle, dan banyak lagi.
### 🌍 Bahasa README
Inggris: [EN](../README.md)
| Arab: [AR](README.ar.md)
| Jerman: [DE](README.de.md)
| Spanyol: [ES](README.es.md)
| Prancis: [FR](README.fr.md)
| Hindi: [HI](README.hi.md)
| Indonesia: [ID](README.id.md)
| Italia: [IT](README.it.md)
| Jepang: [JA](README.ja.md)
| Polandia: [PL](README.pl.md)
| Portugis: [PT](README.pt.md)
| Rusia: [RU](README.ru.md)
| Turki: [TR](README.tr.md)
| Mandarin: [ZH](README.zh.md)
<p align="center">
<a href="#instalasi">Instalasi</a> •
<a href="#fitur">Fitur</a> •
<a href="#penggunaan">Penggunaan</a> •
<a href="#screenshot">Screenshot</a> •
<a href="#troubleshooting">Troubleshooting</a> •
<a href="#sponsor">Sponsor</a> •
<a href="#kontribusi">Kontribusi</a>
</p>
</div>
---
<a id="mengapa-ytsage"></a>
## ❓ Mengapa YTSage?
YTSage dirancang untuk pengguna yang menginginkan **pengunduh YouTube yang sederhana namun kuat**. Tidak seperti alat lainnya, ia menawarkan:
- Antarmuka PySide6 yang modern dan bersih
- Unduh video, audio, dan subtitle sekali klik
- Fitur canggih seperti SponsorBlock, penggabungan subtitle, dan pemilihan playlist
- Mode Generik Opsional untuk situs di luar YouTube yang didukung oleh yt-dlp
- Dukungan lintas platform dan instalasi mudah
<a id="fitur"></a>
## ✨ Fitur
<div align="center">
| Fitur Dasar | Fitur Lanjutan | Fitur Tambahan |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Tabel Format | 🚫 Integrasi SponsorBlock | 🎞️ Tampilan FPS/HDR |
| 🎵 Ekstraksi Audio | 📝 Pemilihan & Penggabungan Subtitle | 🔄 Pembaruan yt-dlp Otomatis |
| ✨ Antarmuka Pengguna Sederhana | 💾 Simpan Deskripsi & Thumbnail | 🛠️ Deteksi FFmpeg/yt-dlp/Deno |
| 📋 Dukungan & Pemilih Playlist | 🚀 Pembatas Kecepatan | ⚙️ Perintah Kustom |
| 📑 Integrasi Bab (Chapters) | ✂️ Potong Bagian Video | 🍪 Login Cookie |
| 📜 Riwayat Unduhan | 🔄 Pilihan Saluran Rilis | 🌐 Dukungan Proxy |
| 🎚️ Konversi Format Audio | 🎬 Pengaturan Format Video | 🆙 Tab Pembaruan Terintegrasi |
| 🌍 Mode Generik | 🔊 Normalisasi Audio (EBU R128) | 🌍 Lokalisasi dalam 14 Bahasa |
| 💾 Ekspor Playlist | ⚙️ Kualitas & Subtitle Default | |
</div>
<a id="instalasi"></a>
## 🚀 Instalasi
### ⚡ Instalasi Cepat (Direkomendasikan)
Instal YTSage melalui PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Perbarui Instalasi yang Ada</summary>
```bash
pip install --upgrade ytsage
```
</details>
Kemudian jalankan aplikasi:
```bash
ytsage
```
### 📦 Executable Siap Pakai (Executable)
> [👉 Unduh Rilis Terbaru](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Format | Deskripsi |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Installer Standar |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Dilengkapi dengan FFmpeg |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Versi Portabel, tidak perlu instalasi |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portabel dengan FFmpeg, dikompresi (ZIP) |
<details>
<summary>🛠️ Langkah Instalasi</summary>
1. **Installer EXE (`.exe`)**: Klik dua kali pada file dan ikuti wizard pengaturan.
2. **Versi Portabel (`.zip`)**: Ekstrak arsip ke lokasi yang diinginkan dan jalankan `ytsage.exe`.
3. **FFmpeg Bawaan**: Jika Anda tidak memiliki FFmpeg di sistem Anda, pilih versi dengan FFmpeg bawaan.
</details>
#### 🐧 Linux
| Format | Deskripsi |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Paket Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, Portabel |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Paket RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Bundel Flatpak |
<details>
<summary>🛠️ Langkah Instalasi</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Jika perlu perbaiki dependensi yang kurang
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Ikuti instruksi di Flathub atau jalankan:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Format | Deskripsi |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Aplikasi ZIP untuk Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Installer Disk Image untuk Apple Silicon |
<details>
<summary>🛠️ Langkah Instalasi</summary>
- **Installer DMG (`.dmg`)**: Klik dua kali untuk memasang, lalu tarik `YTSage.app` ke folder Applications Anda.
- **Arsip Aplikasi (`.zip`)**: Ekstrak ZIP dan pindahkan `YTSage.app` ke folder Applications Anda.
*Catatan: Jika Anda mendapatkan kesalahan "App is damaged", lihat bagian Troubleshooting macOS di bawah ini.*
</details>
---
<details>
<summary>💻 Instalasi Manual dari Sumber (Source)</summary>
### 1. Kloning Repositori
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Instal Dependensi
#### ⚡ Dengan uv
```bash
uv pip install .
```
#### 📦 Atau dengan pip standar
```bash
pip install .
```
### 3. Jalankan Aplikasi
```bash
python -m ytsage.main
```
</details>
<a id="screenshot"></a>
## 📸 Screenshot
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Pengaturan Unduhan" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Unduhan Playlist" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Pengaturan Unduhan</em></td>
<td align="center"><em>Unduhan Playlist</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Pemilihan Format Audio" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Opsi Kustom" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Format Audio</em></td>
<td align="center"><em>Opsi Kustom</em></td>
</tr>
</table>
</div>
<a id="penggunaan"></a>
## 📖 Penggunaan
<details>
<summary>🎯 Penggunaan Dasar</summary>
1. **Luncurkan YTSage**
2. **Tempel URL YouTube** (atau gunakan tombol "Paste URL")
3. **Klik "Analyze"**
4. **Pilih Format:**
- `Video` untuk unduhan video
- `Audio Only` untuk ekstraksi audio
5. **Pilih Opsi:**
- Aktifkan subtitle dan pilih bahasa
- Aktifkan penggabungan subtitle
- Simpan thumbnail
- Hapus bagian sponsor
- Simpan deskripsi
- Masukkan bab (chapters)
6. **Pilih Direktori Output**
7. **Klik "Download"**
> 💡 Direktori unduhan bawaan adalah folder "Downloads" pengguna.
</details>
<details>
<summary>📋 Unduhan Playlist</summary>
1. **Tempel URL Playlist**
2. **Klik "Analyze"**
3. **Pilih video dari pemilih playlist (opsional, default semua)**
4. **Pilih format/kualitas yang diinginkan**
5. **Klik "Download"**
> 💡 Aplikasi secara otomatis mengelola antrean unduhan, dan Anda dapat mengekspor entri playlist sebagai file `.txt`, `.csv`, `.m3u`, atau `.json`.
</details>
<details>
<summary>🌍 Mode Generik untuk Situs Selain YouTube</summary>
Gunakan Mode Generik saat Anda ingin YTSage menerima URL dari situs yang didukung oleh yt-dlp seperti Dailymotion, CBC Gem, TikTok, dan lainnya.
Cara menggunakannya:
1. Buka `Download Settings`.
2. Aktifkan `Generic Mode`.
3. Tempel URL video atau playlist yang didukung selain YouTube.
4. Klik `Analyze`.
5. Pilih format dan unduh seperti biasa.
Catatan:
- Mode Generik hanya mengubah validasi URL di dalam YTSage. Situs target harus tetap didukung oleh versi yt-dlp yang Anda instal.
- Beberapa situs memerlukan cookie, login, proxy, atau argumen yt-dlp tambahan tergantung pada ekstraktornya.
- Jika suatu situs gagal, perbarui yt-dlp dari tab pembaruan terintegrasi sebelum melaporkan masalah.
</details>
<details>
<summary>🧰 Opsi Media & Unduhan</summary>
- **Opsi Subtitle:** Filter bahasa dan masukkan subtitle ke dalam file video.
- **Penggabungan Subtitle:** Menggabungkan subtitle ke dalam file video untuk subtitle permanen (hardcoded).
- **Simpan Deskripsi:** Simpan deskripsi video sebagai file teks.
- **Simpan Thumbnail:** Simpan thumbnail video sebagai file gambar.
- **Masukkan Bab (Chapters):** Sertakan penanda bab sebagai metadata untuk pemutar video yang kompatibel.
- **Hapus Bagian Sponsor:** Gunakan SponsorBlock untuk menghapus segmen sponsor dari video.
- **Potong Video:** Unduh hanya bagian tertentu dari video dengan menentukan rentang waktu dalam format `JJ:MM:DD`.
</details>
<details>
<summary>⚙️ Pengaturan Output & File</summary>
- **Pembatas Kecepatan:** Batasi kecepatan unduhan, misalnya `500K` untuk 500 KB/s.
- **Simpan Jalur Unduhan:** Menyimpan jalur unduhan default untuk unduhan di masa mendatang. Tersedia di **Download Settings → Download Path**.
- **Resolusi Video Default:** Atur resolusi video pilihan Anda untuk pemilihan otomatis (misalnya 1080p, 720p). Tersedia di **Download Settings → Default Video Resolution**.
- **Bahasa Subtitle Default:** Atur bahasa subtitle default untuk pemilihan otomatis (dipisahkan koma, misalnya `id,en`). Tersedia di **Download Settings → Default Subtitle Languages**.
- **Format Nama File:** Sesuaikan format nama file output menggunakan variabel seperti `%(title)s`, `%(uploader)s`, `%(playlist_index)s`, dan `%(resolution)s`. Tersedia di **Download Settings → Filename Format**.
- **Paksa Format Output:** Paksa unduhan video ke format kontainer tertentu seperti `mp4`, `webm`, atau `mkv`. Tersedia di **Download Settings → Output Format Settings**.
- **Konversi Format Audio:** Konversi unduhan audio saja ke format pilihan seperti `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, atau `Best`. Tersedia di **Download Settings → Audio Format Settings**.
- **Normalisasi Audio:** Standarisasi volume untuk unduhan audio saja menggunakan EBU R128.
- **Koneksi Serentak:** Tingkatkan kecepatan unduhan secara signifikan dengan mengunduh file dalam beberapa bagian secara bersamaan. Tersedia di **Download Settings → General → Concurrent Connections** (default 1, maksimal 8-10 direkomendasikan untuk menghindari blokir IP).
</details>
<details>
<summary>🌐 Akses & Jaringan</summary>
- **Login Cookie:** Masuk ke YouTube menggunakan cookie untuk mengakses konten pribadi.
Penggunaan:
1. **Direkomendasikan:** Gunakan opsi bawaan `Extract cookies from browser` di aplikasi, lalu pilih browser dan opsional profil Anda.
2. Secara opsional, ekstrak cookie secara manual:
a. Ekspor cookie dari browser Anda menggunakan ekstensi seperti [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Salin cookie dalam format Netscape
c. Buat file bernama `cookies.txt` dan tempel cookie
d. Pilih file `cookies.txt` di aplikasi
- **Dukungan Proxy:** Gunakan server proxy untuk unduhan, misalnya `http://<server-proxy>:<port>`
- **Mode Generik:** Izinkan YTSage untuk menganalisis dan mengunduh dari situs selain YouTube yang didukung oleh yt-dlp. Aktifkan dari **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Alat & Pemeliharaan</summary>
- **Perintah Kustom:** Akses fitur yt-dlp tingkat lanjut melalui argumen baris perintah.
- **Tab Pembaruan:** Kelola alat pembaruan bawaan dari satu tempat di Opsi Kustom:
- **Pembaruan yt-dlp:** Periksa pembaruan dan beralih antara saluran rilis Stable dan Nightly.
- **Pemeriksa Versi FFmpeg:** Verifikasi versi FFmpeg Anda dan buka panduan instalasi.
- **Pembaruan Deno:** Periksa dan perbarui runtime Deno.
- **Deteksi FFmpeg/yt-dlp/Deno:** Secara otomatis mendeteksi jalur dan versi FFmpeg, yt-dlp, dan Deno dari dialog About.
- **Riwayat Unduhan:** Lihat unduhan sebelumnya dengan thumbnail dan status dari tombol **History**.
</details>
<details>
<summary>🌍 Lokalisasi</summary>
YTSage mendukung **14 bahasa** untuk jangkauan global. Pilih bahasa pilihan Anda di **Custom Options → Language**.
### Bahasa yang Didukung
| Bahasa | Kode | Bahasa | Kode |
|----------|------|----------|------|
| 🇺🇸 Inggris | `en` | 🇪🇸 Spanyol | `es` |
| 🇸🇦 Arab | `ar` | 🇫🇷 Prancis | `fr` |
| 🇩🇪 Jerman | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonesia | `id` | 🇮🇹 Italia | `it` |
| 🇯🇵 Jepang | `ja` | 🇵🇱 Polandia | `pl` |
| 🇧🇷 Portugis | `pt` | 🇷🇺 Rusia | `ru` |
| 🇹🇷 Turki | `tr` | 🇨🇳 Mandarin | `zh` |
### Terjemahan README
| Bahasa | File | Bahasa | File |
|----------|------|----------|------|
| 🇺🇸 Inggris | [README.md](../README.md) | 🇪🇸 Spanyol | [README.es.md](README.es.md) |
| 🇸🇦 Arab | [README.ar.md](README.ar.md) | 🇫🇷 Prancis | [README.fr.md](README.fr.md) |
| 🇩🇪 Jerman | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonesia | [README.id.md](README.id.md) | 🇮🇹 Italia | [README.it.md](README.it.md) |
| 🇯🇵 Jepang | [README.ja.md](README.ja.md) | 🇵🇱 Polandia | [README.pl.md](README.pl.md) |
| 🇧🇷 Portugis | [README.pt.md](README.pt.md) | 🇷🇺 Rusia | [README.ru.md](README.ru.md) |
| 🇹🇷 Turki | [README.tr.md](README.tr.md) | 🇨🇳 Mandarin | [README.zh.md](README.zh.md) |
> 💡 **Ingin membantu menerjemahkan?** Lihat bagian [Kontribusi](#kontribusi) untuk membantu kami menambahkan lebih banyak bahasa!
</details>
<a id="troubleshooting"></a>
## 🛠️ Troubleshooting
<details>
<summary>Klik untuk melihat masalah umum dan solusinya</summary>
- **Tabel format tidak muncul:** Perbarui yt-dlp ke versi terbaru dan coba beralih ke yt-dlp Nightly.
- **Unduhan gagal:** Periksa koneksi internet Anda dan pastikan video tersedia.
- **Kesalahan Unduhan Spesifik:**
- **Video Pribadi:** Gunakan autentikasi cookie untuk mengakses konten pribadi.
- **Konten Dibatasi Usia:** Masuk ke akun YouTube Anda untuk melihat video yang dibatasi usia.
- **Video yang Diblokir Geo:** Pertimbangkan menggunakan VPN untuk melewati batasan regional.
- **Video Dihapus:** Video tidak lagi tersedia di YouTube.
- **Live Stream:** Streaming langsung tidak dapat diunduh saat sedang disiarkan; tunggu hingga streaming selesai.
- **Kesalahan Jaringan:** Periksa koneksi internet Anda dan coba lagi.
- **URL Tidak Valid:** Pastikan URL benar dan berasal dari platform yang didukung.
- **Konten Premium:** Memerlukan langganan YouTube Premium.
- **Blokir Hak Cipta:** Konten diblokir karena pembatasan hak cipta.
- **File video dan audio terpisah setelah diunduh:** Ini terjadi ketika FFmpeg hilang atau tidak terdeteksi. YTSage memerlukan FFmpeg untuk menggabungkan aliran video dan audio berkualitas tinggi.
- **Solusi:** Pastikan FFmpeg terinstal dan dapat diakses di PATH sistem Anda. Untuk pengguna Windows, opsi termudah adalah mengunduh file `YTSage-v<version>-ffmpeg.exe`, yang dilengkapi dengan FFmpeg.
---
#### 🛡️ Peringatan Windows Defender / Antivirus
Beberapa perangkat lunak antivirus mungkin menandai file `.exe` sebagai positif palsu (false positive). Ini adalah **batasan umum** dari aplikasi yang dipaketkan.
**Mengapa ini terjadi:**
- Heuristik antivirus mungkin salah mengidentifikasi executable yang dipaketkan sebagai mencurigakan.
**Opsi Aman:**
- ✅ **Gunakan instalasi pip:** `pip install ytsage` (direkomendasikan)
- ✅ **Build dari sumber**: Mengikuti [panduan](../.github/CI_CD_README.md) ini
- ✅ **Whitelist aplikasi** di perangkat lunak antivirus Anda.
#### 🍎 macOS: "App is damaged and cant be opened"
Jika Anda melihat kesalahan ini di macOS Sonoma atau yang lebih baru, Anda perlu menghapus atribut karantina.
1. **Buka Terminal** (Anda dapat menemukannya menggunakan Spotlight).
2. **Ketik perintah berikut** tetapi **JANGAN** tekan Enter dulu. Pastikan untuk menyertakan spasi di akhir:
```bash
xattr -d com.apple.quarantine
```
3. **Tarik file `YTSage.app` dari jendela Finder Anda** dan lepaskan langsung ke jendela Terminal. Ini akan secara otomatis menempelkan jalur file yang benar.
4. **Tekan Enter** untuk menjalankan perintah.
5. **Coba buka kembali YTSage.app.** Sekarang seharusnya dapat diluncurkan dengan benar.
---
#### **Lokasi Konfigurasi (Lanjutan)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Sponsor
Jika YTSage menghemat waktu Anda, pertimbangkan untuk mensponsori proyek ini. Sponsor membantu mencakup waktu pengembangan, pengujian di semua platform, dan peningkatan di masa mendatang.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Tautan sponsor tersedia langsung melalui dialog About di dalam aplikasi.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="kontribusi"></a>
## 👥 Kontribusi
Kami menerima kontribusi! Berikut cara Anda dapat membantu:
1. 🍴 Fork repositori
2. 🌿 Buat cabang fitur Anda:
```bash
git checkout -b feature/FiturLuarBiasa
```
3. 💾 Komit perubahan Anda:
```bash
git commit -m 'Tambah FiturLuarBiasa'
```
4. 📤 Push ke cabang:
```bash
git push origin feature/FiturLuarBiasa
```
5. 🔄 Buka Pull Request
### 🌍 Berkontribusi pada Terjemahan
- Perbarui file README lokal yang relevan (misalnya `readme-translations/README.id.md`)
- Jaga agar string aplikasi tetap sinkron dengan mengedit `ytsage/languages/<code>.json`
- Jika bahasa Anda belum ada, mulailah dari `README.md` dan buat `README.<code>.md`
<details>
<summary>📂 Struktur Proyek</summary>
## YTSage - Struktur Proyek
Dokumen ini menjelaskan struktur folder yang terorganisir dari YTSage.
### 📁 Struktur Proyek
```
YTSage/
├── 📁 .github/ # Konfigurasi GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Templat Issue
│ │ └── 🐛-bug-report.md # Templat laporan bug
│ ├─── 📁 workflows/ # Alur kerja GitHub Actions
│ │ ├── build-linux.yml # Alur kerja build Linux
│ │ ├── build-macos.yml # Alur kerja build macOS
│ │ │── build-windows.yml # Alur kerja build Windows
| | └── release-all.yml # Alur kerja rilis master
│ └── 📄 CI_CD_README.md # Dokumentasi CI/CD
├── 📁 branding/ # Aset branding (screenshot, SVG)
│ ├── 📁 icons/ # Ikon aplikasi
│ ├── 📁 screenshots/ # Screenshot untuk dokumentasi
│ └── 📁 svg/ # Aset SVG
├── 📄 LICENSE # File lisensi
├── 📄 pyproject.toml # Metadata proyek dan dependensi
├── 📄 README.md # Dokumentasi proyek
├── 📄 requirements.txt # Dependensi Python (dev)
└── 📁 ytsage/ # Paket kode sumber
├── 📁 assets/ # Aset runtime
│ ├── 📁 Icon/ # Ikon aplikasi
│ └── 📁 sound/ # File audio
├── 📁 languages/ # File lokalisasi
│ ├── 📄 ar.json # Terjemahan Arab
│ ├── 📄 de.json # Terjemahan Jerman
│ ├── 📄 en.json # Terjemahan Inggris
│ └── ... # Bahasa lainnya
├── 📁 core/ # Logika bisnis inti
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Integrasi Deno
│ ├── 📄 ytsage_downloader.py # Fungsionalitas pengunduhan
│ ├── 📄 ytsage_ffmpeg.py # Integrasi FFmpeg
│ ├── 📄 ytsage_utils.py # Fungsi utilitas
│ └── 📄 ytsage_yt_dlp.py # Integrasi yt-dlp
├── 📁 gui/ # Komponen antarmuka pengguna
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # Jendela utama aplikasi
│ └── 📁 ytsage_gui_dialogs/ # Kelas dialog
├── 📁 utils/ # Modul utilitas
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # Manajemen konfigurasi
│ └── 📄 ytsage_logger.py # Alat logging
├── 📄 __init__.py # Titik masuk paket
└── 📄 main.py # Skrip eksekusi utama
```
</details>
## ⭐️ Riwayat Bintang
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Lisensi
Proyek ini dilisensikan di bawah Lisensi MIT - lihat file [LICENSE](../LICENSE) untuk detailnya.
## 🙏 Terima Kasih
<details>
<summary>Tampilkan Terima Kasih</summary>
<div align="center">
<p>Terima kasih banyak kepada semua orang yang telah berkontribusi pada proyek ini dengan membuka masalah untuk menyarankan perbaikan atau melaporkan bug.</p>
<table>
<tr class="section"><th colspan="2">Komponen Utama</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Mesin Pengunduhan</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Pemrosesan Media</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime untuk integrasi yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Pustaka & Framework</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>Framework GUI</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Pemrosesan Gambar</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>Permintaan HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Manajemen Versi & Pemaketan</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Rendering Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td>
</tr>
<tr class="section"><th colspan="2">Aset & Kontributor</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 oleh Universfield</a></td>
<td>Suara Notifikasi</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Kontributor Kode</td>
</tr>
</table>
</div>
</details>
## ⚠️ Penafian
Alat ini hanya untuk penggunaan pribadi. Harap hormati Ketentuan Layanan YouTube dan hak-hak produser konten.
---
<div align="center">
Dibuat dengan ❤️ oleh [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Un downloader YouTube moderno con un'interfaccia PySide6 pulita.**
Scarica video in qualsiasi qualità, estrai audio, ottieni sottotitoli e molto altro.
### 🌍 Lingue del README
Inglese: [EN](../README.md)
| Arabo: [AR](README.ar.md)
| Tedesco: [DE](README.de.md)
| Spagnolo: [ES](README.es.md)
| Francese: [FR](README.fr.md)
| Hindi: [HI](README.hi.md)
| Indonesiano: [ID](README.id.md)
| Italiano: [IT](README.it.md)
| Giapponese: [JA](README.ja.md)
| Polacco: [PL](README.pl.md)
| Portoghese: [PT](README.pt.md)
| Russo: [RU](README.ru.md)
| Turco: [TR](README.tr.md)
| Cinese: [ZH](README.zh.md)
<p align="center">
<a href="#installazione">Installazione</a> •
<a href="#caratteristiche">Caratteristiche</a> •
<a href="#utilizzo">Utilizzo</a> •
<a href="#screenshot">Screenshot</a> •
<a href="#risoluzione-dei-problemi">Risoluzione dei problemi</a> •
<a href="#sponsor">Sponsor</a> •
<a href="#contribuire">Contribuire</a>
</p>
</div>
---
<a id="perché-ytsage"></a>
## ❓ Perché YTSage?
YTSage è progettato per gli utenti che desiderano un **downloader YouTube semplice ma potente**. A differenza di altri strumenti, offre:
- Un'interfaccia PySide6 moderna e pulita
- Download di video, audio e sottotitoli con un solo clic
- Funzioni avanzate come SponsorBlock, fusione dei sottotitoli e selezione della playlist
- Modalità Generica opzionale per siti diversi da YouTube supportati da yt-dlp
- Supporto multipiattaforma e installazione semplice
<a id="caratteristiche"></a>
## ✨ Caratteristiche
<div align="center">
| Funzioni Base | Funzioni Avanzate | Funzioni Extra |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Tabella Formati | 🚫 Integrazione SponsorBlock | 🎞️ Visualizzazione FPS/HDR |
| 🎵 Estrazione Audio | 📝 Selezione e Fusione Sottotitoli | 🔄 Aggiornamento automatico yt-dlp |
| ✨ Interfaccia Utente Semplice | 💾 Salva Descrizione e Anteprima | 🛠️ Rilevamento FFmpeg/yt-dlp/Deno |
| 📋 Supporto e Selettore Playlist | 🚀 Limitatore di Velocità | ⚙️ Comandi Personalizzati |
| 📑 Integrazione Capitoli | ✂️ Ritaglio Sezioni Video | 🍪 Login tramite Cookie |
| 📜 Cronologia Download | 🔄 Scelta del Canale di Rilascio | 🌐 Supporto Proxy |
| 🎚️ Conversione Formato Audio | 🎬 Impostazioni Formato Video | 🆙 Tab Aggiornamento Integrato |
| 🌍 Modalità Generica | 🔊 Normalizzazione Audio (EBU R128) | 🌍 Localizzazione in 14 lingue |
| 💾 Esportazione Playlist | ⚙️ Qualità e Sottotitoli Predefiniti | |
</div>
<a id="installazione"></a>
## 🚀 Installazione
### ⚡ Installazione Rapida (Consigliata)
Installa YTSage tramite PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Aggiornare un'installazione esistente</summary>
```bash
pip install --upgrade ytsage
```
</details>
Quindi esegui l'applicazione:
```bash
ytsage
```
### 📦 Eseguibili Pre-compilati (Executable)
> [👉 Scarica l'ultima versione](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Formato | Descrizione |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Installer Standard |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Include FFmpeg |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Versione Portable, nessuna installazione richiesta |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portable con FFmpeg, compresso (ZIP) |
<details>
<summary>🛠️ Passaggi per l'installazione</summary>
1. **Installer EXE (`.exe`)**: Fare doppio clic sul file e seguire la procedura guidata.
2. **Versione Portable (`.zip`)**: Estrarre l'archivio nella posizione desiderata ed eseguire `ytsage.exe`.
3. **FFmpeg integrato**: Se non hai FFmpeg sul tuo sistema, scegli le versioni con FFmpeg integrato.
</details>
#### 🐧 Linux
| Formato | Descrizione |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Pacchetto Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, Portable |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Pacchetto RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Bundle Flatpak |
<details>
<summary>🛠️ Passaggi per l'installazione</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Se necessario per correggere le dipendenze mancanti
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Seguire le istruzioni su Flathub o eseguire:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Formato | Descrizione |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | App ZIP per Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Installer Disk Image per Apple Silicon |
<details>
<summary>🛠️ Passaggi per l'installazione</summary>
- **Installer DMG (`.dmg`)**: Fare doppio clic per montare, quindi trascinare `YTSage.app` nella cartella Applicazioni.
- **Archivio App (`.zip`)**: Estrarre lo ZIP e spostare `YTSage.app` nella cartella Applicazioni.
*Nota: Se ricevi l'errore "L'app è danneggiata", consulta la sezione Risoluzione dei problemi macOS di seguito.*
</details>
---
<details>
<summary>💻 Installazione manuale dai sorgenti</summary>
### 1. Clonare il repository
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Installare le dipendenze
#### ⚡ Con uv
```bash
uv pip install .
```
#### 📦 O con pip standard
```bash
pip install .
```
### 3. Eseguire l'applicazione
```bash
python -m ytsage.main
```
</details>
<a id="screenshot"></a>
## 📸 Screenshot
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Impostazioni Download" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Download Playlist" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Impostazioni Download</em></td>
<td align="center"><em>Download Playlist</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Selezione Formato Audio" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Opzioni Personalizzate" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Formato Audio</em></td>
<td align="center"><em>Opzioni Personalizzate</em></td>
</tr>
</table>
</div>
<a id="utilizzo"></a>
## 📖 Utilizzo
<details>
<summary>🎯 Utilizzo Base</summary>
1. **Avvia YTSage**
2. **Incolla un URL di YouTube** (o usa il pulsante "Incolla URL")
3. **Clicca su "Analizza"**
4. **Scegli il formato:**
- `Video` per il download del video
- `Solo Audio` per l'estrazione dell'audio
5. **Seleziona le opzioni:**
- Abilita i sottotitoli e scegli la lingua
- Abilita la fusione dei sottotitoli
- Salva l'anteprima (thumbnail)
- Rimuovi sezioni sponsor
- Salva la descrizione
- Incorpora i capitoli
6. **Scegli la directory di destinazione**
7. **Clicca su "Download"**
> 💡 La directory di download predefinita è la cartella "Download" dell'utente.
</details>
<details>
<summary>📋 Download Playlist</summary>
1. **Incolla l'URL della playlist**
2. **Clicca su "Analizza"**
3. **Seleziona i video dal selettore (opzionale, tutti per impostazione predefinita)**
4. **Scegli il formato/qualità desiderati**
5. **Clicca su "Download"**
> 💡 L'applicazione gestisce automaticamente la coda di download e puoi esportare le voci della playlist come file `.txt`, `.csv`, `.m3u` o `.json`.
</details>
<details>
<summary>🌍 Modalità Generica per siti non YouTube</summary>
Usa la Modalità Generica quando vuoi che YTSage accetti URL da siti supportati da yt-dlp come Dailymotion, CBC Gem, TikTok e altri.
Come usarla:
1. Apri `Impostazioni Download`.
2. Abilita `Modalità Generica`.
3. Incolla un URL di un video o di una playlist supportata che non sia YouTube.
4. Clicca su `Analizza`.
5. Scegli un formato e scarica normalmente.
Note:
- La Modalità Generica cambia solo la validazione dell'URL all'interno di YTSage. Il sito di destinazione deve essere comunque supportato dalla versione di yt-dlp installata.
- Alcuni siti richiedono cookie, login, proxy o argomenti yt-dlp aggiuntivi a seconda dell'estrattore.
- Se un sito fallisce, aggiorna yt-dlp dal tab di aggiornamento integrato prima di segnalare il problema.
</details>
<details>
<summary>🧰 Opzioni Media e Download</summary>
- **Opzioni Sottotitoli:** Filtra le lingue e incorpora i sottotitoli nel file video.
- **Fusione Sottotitoli:** Fonde i sottotitoli nel file video per sottotitoli permanenti (hardcoded).
- **Salva Descrizione:** Salva la descrizione del video come file di testo.
- **Salva Anteprima:** Salva l'anteprima (thumbnail) del video come file immagine.
- **Incorpora Capitoli:** Include i segnaposti dei capitoli come metadati per i lettori video compatibili.
- **Rimuovi Sezioni Sponsor:** Usa SponsorBlock per rimuovere i segmenti sponsorizzati dal video.
- **Ritaglia Video:** Scarica solo parti specifiche del video specificando un intervallo di tempo nel formato `HH:MM:SS`.
</details>
<details>
<summary>⚙️ Impostazioni Output e File</summary>
- **Limitatore di Velocità:** Limita la velocità di download, ad esempio `500K` per 500 KB/s.
- **Salva Percorso Download:** Salva il percorso di download predefinito per i download futuri. Disponibile in **Impostazioni Download → Percorso Download**.
- **Risoluzione Video Predefinita:** Imposta la risoluzione video preferita per la selezione automatica (es. 1080p, 720p). Disponibile in **Impostazioni Download → Risoluzione Video Predefinita**.
- **Lingue Sottotitoli Predefinite:** Imposta le lingue dei sottotitoli predefinite per la selezione automatica (separate da virgola, es. `it,en`). Disponibile in **Impostazioni Download → Lingue Sottotitoli Predefinite**.
- **Formato Nome File:** Personalizza il formato del nome del file di output utilizzando variabili come `%(title)s`, `%(uploader)s`, `%(playlist_index)s` e `%(resolution)s`. Disponibile in **Impostazioni Download → Formato Nome File**.
- **Forza Formato Output:** Forza il download del video in un formato contenitore specifico come `mp4`, `webm` o `mkv`. Disponibile in **Impostazioni Download → Impostazioni Formato Output**.
- **Conversione Formato Audio:** Converte i download di solo audio nel formato preferito come `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, o `Best`. Disponibile in **Impostazioni Download → Impostazioni Formato Audio**.
- **Normalizzazione Audio:** Standardizza il volume per i download di solo audio utilizzando EBU R128.
- **Connessioni Simultanee:** Aumenta significativamente la velocità di download scaricando i file in più parti contemporaneamente. Disponibile in **Impostazioni Download → Generali → Connessioni Simultanee** (predefinito 1, massimo 8-10 consigliato per evitare blocchi IP).
</details>
<details>
<summary>🌐 Accesso e Rete</summary>
- **Login tramite Cookie:** Accedi a YouTube utilizzando i cookie per accedere ai contenuti privati.
Utilizzo:
1. **Consigliato:** Usa l'opzione integrata nell'app `Estrai cookie dal browser`, quindi scegli il browser e opzionalmente il profilo.
2. In alternativa, estrai i cookie manualmente:
a. Esporta i cookie dal tuo browser utilizzando un'estensione come [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Copia i cookie nel formato Netscape
c. Crea un file chiamato `cookies.txt` e incolla i cookie
d. Seleziona il file `cookies.txt` nell'app
- **Supporto Proxy:** Usa un server proxy per i download, ad esempio `http://<server-proxy>:<porta>`
- **Modalità Generica:** Consente a YTSage di analizzare e scaricare da siti non YouTube supportati da yt-dlp. Abilitalo da **Impostazioni Download → Modalità Generica**.
</details>
<details>
<summary>🛠️ Strumenti e Manutenzione</summary>
- **Comandi Personalizzati:** Accedi alle funzioni avanzate di yt-dlp tramite argomenti della riga di comando.
- **Tab Aggiornamento:** Gestisci gli strumenti di aggiornamento integrati da un unico posto nelle Opzioni Personalizzate:
- **Aggiornamento yt-dlp:** Controlla gli aggiornamenti e passa tra i canali di rilascio Stabile e Nightly.
- **Controllo Versione FFmpeg:** Verifica la tua versione di FFmpeg e apri le guide all'installazione.
- **Aggiornamento Deno:** Controlla e aggiorna il runtime Deno.
- **Rilevamento FFmpeg/yt-dlp/Deno:** Rileva automaticamente i percorsi e le versioni di FFmpeg, yt-dlp e Deno dal dialogo Informazioni.
- **Cronologia Download:** Visualizza i download passati con anteprime e stati dal pulsante **Cronologia**.
</details>
<details>
<summary>🌍 Localizzazione</summary>
YTSage supporta **14 lingue** per una portata globale. Scegli la tua lingua preferita in **Opzioni Personalizzate → Lingua**.
### Lingue Supportate
| Lingua | Codice | Lingua | Codice |
|----------|------|----------|------|
| 🇺🇸 Inglese | `en` | 🇪🇸 Spagnolo | `es` |
| 🇸🇦 Arabo | `ar` | 🇫🇷 Francese | `fr` |
| 🇩🇪 Tedesco | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonesiano | `id` | 🇮🇹 Italiano | `it` |
| 🇯🇵 Giapponese | `ja` | 🇵🇱 Polacco | `pl` |
| 🇧🇷 Portoghese | `pt` | 🇷🇺 Russo | `ru` |
| 🇹🇷 Turco | `tr` | 🇨🇳 Cinese | `zh` |
### Traduzioni del README
| Lingua | File | Lingua | File |
|----------|------|----------|------|
| 🇺🇸 Inglese | [README.md](../README.md) | 🇪🇸 Spagnolo | [README.es.md](README.es.md) |
| 🇸🇦 Arabo | [README.ar.md](README.ar.md) | 🇫🇷 Francese | [README.fr.md](README.fr.md) |
| 🇩🇪 Tedesco | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonesiano | [README.id.md](README.id.md) | 🇮🇹 Italiano | [README.it.md](README.it.md) |
| 🇯🇵 Giapponese | [README.ja.md](README.ja.md) | 🇵🇱 Polacco | [README.pl.md](README.pl.md) |
| 🇧🇷 Portoghese | [README.pt.md](README.pt.md) | 🇷🇺 Russo | [README.ru.md](README.ru.md) |
| 🇹🇷 Turco | [README.tr.md](README.tr.md) | Cinese | [README.zh.md](README.zh.md) |
> 💡 **Vuoi aiutare con la traduzione?** Consulta la sezione [Contribuire](#contribuire) per aiutarci ad aggiungere altre lingue!
</details>
<a id="risoluzione-dei-problemi"></a>
## 🛠️ Risoluzione dei problemi
<details>
<summary>Clicca per visualizzare i problemi comuni e le soluzioni</summary>
- **La tabella dei formati non appare:** Aggiorna yt-dlp all'ultima versione e prova a passare a yt-dlp Nightly.
- **Download fallito:** Controlla la tua connessione internet e assicurati che il video sia disponibile.
- **Errori di download specifici:**
- **Video Privati:** Usa l'autenticazione tramite cookie per accedere ai contenuti privati.
- **Contenuti con limiti di età:** Accedi al tuo account YouTube per visualizzare i video con limiti di età.
- **Video bloccati geograficamente:** Considera l'uso di una VPN per aggirare le restrizioni regionali.
- **Video rimosso:** Il video non è più disponibile su YouTube.
- **Live Stream:** I live stream non possono essere scaricati durante la trasmissione; attendi la fine dello stream.
- **Errori di rete:** Controlla la tua connessione internet e riprova.
- **URL non valido:** Assicurati che l'URL sia corretto e provenga da una piattaforma supportata.
- **Contenuti Premium:** Richiede un abbonamento YouTube Premium.
- **Blocco Copyright:** Il contenuto è bloccato a causa di restrizioni sul copyright.
- **I file video e audio sono separati dopo il download:** Questo accade quando FFmpeg manca o non viene rilevato. YTSage richiede FFmpeg per unire i flussi video e audio di alta qualità.
- **Soluzione:** Assicurati che FFmpeg sia installato e accessibile nel PATH del tuo sistema. Per gli utenti Windows, l'opzione più semplice è scaricare il file `YTSage-v<versione>-ffmpeg.exe`, che include FFmpeg.
---
#### 🛡️ Avviso Windows Defender / Antivirus
Alcuni software antivirus potrebbero contrassegnare i file `.exe` come falsi positivi. Questa è una **limitazione nota** delle applicazioni pacchettizzate.
**Perché succede:**
- Le euristiche dell'antivirus potrebbero identificare erroneamente gli eseguibili pacchettizzati come sospetti.
**Opzioni sicure:**
- ✅ **Usa l'installazione tramite pip:** `pip install ytsage` (consigliato)
- ✅ **Compila dai sorgenti**: Seguendo questa [guida](../.github/CI_CD_README.md)
- ✅ **Aggiungi l'applicazione alla whitelist** nel tuo software antivirus.
#### 🍎 macOS: "Lapp è danneggiata e non può essere aperta"
Se visualizzi questo errore su macOS Sonoma o versioni successive, devi rimuovere l'attributo di quarantena.
1. **Apri il Terminale** (puoi trovarlo usando Spotlight).
2. **Digita il seguente comando** ma **NON** premere ancora Invio. Assicurati di includere lo spazio alla fine:
```bash
xattr -d com.apple.quarantine
```
3. **Trascina il file `YTSage.app` dalla finestra del Finder** e rilascialo direttamente nella finestra del Terminale. Questo incollerà automaticamente il percorso corretto del file.
4. **Premi Invio** per eseguire il comando.
5. **Prova a riaprire YTSage.app.** Ora dovrebbe avviarsi correttamente.
---
#### **Percorsi di configurazione (Avanzato)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Sponsor
Se YTSage ti fa risparmiare tempo, considera di sponsorizzare il progetto. Le sponsorizzazioni aiutano a coprire il tempo di sviluppo, i test su tutte le piattaforme e i miglioramenti futuri.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Il link per la sponsorizzazione è disponibile direttamente tramite il dialogo Informazioni all'interno dell'app.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contribuire"></a>
## 👥 Contribuire
I contributi sono benvenuti! Ecco come puoi aiutare:
1. 🍴 Fork del repository
2. 🌿 Crea il tuo ramo per la funzione:
```bash
git checkout -b feature/FunzioneIncredibile
```
3. 💾 Fai il commit delle tue modifiche:
```bash
git commit -m 'Aggiungi FunzioneIncredibile'
```
4. 📤 Fai il push sul ramo:
```bash
git push origin feature/FunzioneIncredibile
```
5. 🔄 Apri una Pull Request
### 🌍 Contribuire alle traduzioni
- Aggiorna il file README localizzato pertinente (es. `readme-translations/README.it.md`)
- Mantieni sincronizzate le stringhe dell'app modificando `ytsage/languages/<code>.json`
- Se la tua lingua manca, parti da `README.md` e crea `README.<code>.md`
<details>
<summary>📂 Struttura del Progetto</summary>
## YTSage - Struttura del Progetto
Questo documento descrive la struttura organizzata delle cartelle di YTSage.
### 📁 Struttura del Progetto
```
YTSage/
├── 📁 .github/ # Configurazioni GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Modelli per segnalazioni
│ │ └── 🐛-bug-report.md # Modello per segnalazione bug
│ ├─── 📁 workflows/ # Workflow GitHub Actions
│ │ ├── build-linux.yml # Workflow build Linux
│ │ ├── build-macos.yml # Workflow build macOS
│ │ │── build-windows.yml # Workflow build Windows
| | └── release-all.yml # Workflow release master
│ └── 📄 CI_CD_README.md # Documentazione CI/CD
├── 📁 branding/ # Asset di branding (screenshot, SVG)
│ ├── 📁 icons/ # Icone dell'app
│ ├── 📁 screenshots/ # Screenshot per la documentazione
│ └── 📁 svg/ # Asset SVG
├── 📄 LICENSE # File della licenza
├── 📄 pyproject.toml # Metadati del progetto e dipendenze
├── 📄 README.md # Documentazione del progetto
├── 📄 requirements.txt # Dipendenze Python (dev)
└── 📁 ytsage/ # Pacchetto del codice sorgente
├── 📁 assets/ # Asset runtime
│ ├── 📁 Icon/ # Icone dell'app
│ └── 📁 sound/ # File audio
├── 📁 languages/ # File di localizzazione
│ ├── 📄 ar.json # Traduzione araba
│ ├── 📄 de.json # Traduzione tedesca
│ ├── 📄 en.json # Traduzione inglese
│ └── ... # Altre lingue
├── 📁 core/ # Logica di business principale
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Integrazione Deno
│ ├── 📄 ytsage_downloader.py # Funzionalità di download
│ ├── 📄 ytsage_ffmpeg.py # Integrazione FFmpeg
│ ├── 📄 ytsage_utils.py # Funzioni di utilità
│ └── 📄 ytsage_yt_dlp.py # Integrazione yt-dlp
├── 📁 gui/ # Componenti dell'interfaccia utente
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # Finestra principale dell'app
│ └── 📁 ytsage_gui_dialogs/ # Classi per i dialoghi
├── 📁 utils/ # Moduli di utilità
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # Gestione configurazione
│ └── 📄 ytsage_logger.py # Strumenti di logging
├── 📄 __init__.py # Punto di ingresso del pacchetto
└── 📄 main.py # Script di esecuzione principale
```
</details>
## ⭐️ Cronologia Stelle
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Licenza
Questo progetto è rilasciato sotto Licenza MIT - vedi il file [LICENSE](../LICENSE) per i dettagli.
## 🙏 Ringraziamenti
<details>
<summary>Mostra Ringraziamenti</summary>
<div align="center">
<p>Un grande ringraziamento a tutti coloro che hanno contribuito a questo progetto aprendo segnalazioni per suggerire miglioramenti o segnalare bug.</p>
<table>
<tr class="section"><th colspan="2">Componenti Principali</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Motore di Download</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Elaborazione Media</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime per l'integrazione di yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Librerie e Framework</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>Framework GUI</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Elaborazione Immagini</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>Richieste HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Gestione Versioni e Packaging</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Rendering Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logging</td>
</tr>
<tr class="section"><th colspan="2">Asset e Collaboratori</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 di Universfield</a></td>
<td>Suono di Notifica</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Collaboratore al Codice</td>
</tr>
</table>
</div>
</details>
## ⚠️ Disclaimer
Questo strumento è solo per uso personale. Rispettate i Termini di Servizio di YouTube e i diritti dei produttori di contenuti.
---
<div align="center">
Creato con ❤️ da [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**クリーンなPySide6インターフェースを備えたモダンなYouTubeダウンローダー。**
あらゆる品質でのビデオダウンロード、オーディオ抽出、字幕取得などが可能です。
### 🌍 README 言語
英語: [EN](../README.md)
| アラビア語: [AR](README.ar.md)
| ドイツ語: [DE](README.de.md)
| スペイン語: [ES](README.es.md)
| フランス語: [FR](README.fr.md)
| ヒンディー語: [HI](README.hi.md)
| インドネシア語: [ID](README.id.md)
| イタリア語: [IT](README.it.md)
| 日本語: [JA](README.ja.md)
| ポーランド語: [PL](README.pl.md)
| ポルトガル語: [PT](README.pt.md)
| ロシア語: [RU](README.ru.md)
| トルコ語: [TR](README.tr.md)
| 中国語: [ZH](README.zh.md)
<p align="center">
<a href="#インストール">インストール</a> •
<a href="#機能">機能</a> •
<a href="#使い方">使い方</a> •
<a href="#スクリーンショット">スクリーンショット</a> •
<a href="#トラブルシューティング">トラブルシューティング</a> •
<a href="#スポンサー">スポンサー</a> •
<a href="#貢献">貢献</a>
</p>
</div>
---
<a id="なぜytsageなのか"></a>
## ❓ なぜ YTSage なのか?
YTSageは、**シンプルでありながら強力なYouTubeダウンローダー**を求めるユーザーのために設計されています。他のツールとは異なり、以下の機能を提供します:
- モダンでクリーンなPySide6インターフェース
- ビデオ、オーディオ、字幕のワンクリックダウンロード
- SponsorBlock、字幕結合、プレイリスト選択などの高度な機能
- yt-dlpがサポートするYouTube以外のサイト向けのオプションのジェネリックモード
- クロスプラットフォーム対応と簡単なインストール
<a id="機能"></a>
## ✨ 機能
<div align="center">
| 基本機能 | 高度な機能 | 追加機能 |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 フォーマットテーブル | 🚫 SponsorBlock統合 | 🎞️ FPS/HDR表示 |
| 🎵 オーディオ抽出 | 📝 字幕選択と結合 | 🔄 yt-dlp自動更新 |
| ✨ シンプルなユーザーインターフェース | 💾 説明とサムネイルの保存 | 🛠️ FFmpeg/yt-dlp/Deno検出 |
| 📋 プレイリストのサポートと選択 | 🚀 速度制限 | ⚙️ カスタムコマンド |
| 📑 チャプター統合 | ✂️ ビデオセクションのトリミング | 🍪 クッキーログイン |
| 📜 ダウンロード履歴 | 🔄 リリースチャネルの選択 | 🌐 プロキシサポート |
| 🎚️ オーディオフォーマット変換 | 🎬 ビデオフォーマット設定 | 🆙 統合アップデートタブ |
| 🌍 ジェネリックモード | 🔊 オーディオノーマライズ (EBU R128) | 🌍 14言語へのローカライズ |
| 💾 プレイリストのエクスポート | ⚙️ デフォルトの品質と字幕 | |
</div>
<a id="インストール"></a>
## 🚀 インストール
### ⚡ クイックインストール (推奨)
PyPI経由でYTSageをインストールします:
```bash
pip install ytsage
```
<details>
<summary>🔄 既存のインストールを更新する</summary>
```bash
pip install --upgrade ytsage
```
</details>
その後、アプリケーションを実行します:
```bash
ytsage
```
### 📦 ビルド済み実行ファイル (Executable)
> [👉 最新リリースをダウンロード](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| フォーマット | 説明 |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | 標準インストーラー |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | FFmpeg同梱 |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | ポータブル版、インストール不要 |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | FFmpeg同梱、ポータブル版 (ZIP縮小) |
<details>
<summary>🛠️ インストール手順</summary>
1. **EXE インストーラー (`.exe`)**: ファイルをダブルクリックし、セットアップウィザードに従います。
2. **ポータブル版 (`.zip`)**: アーカイブを任意の場所に展開し、`ytsage.exe` を実行します。
3. **内蔵 FFmpeg**: システムに FFmpeg がインストールされていない場合は、FFmpeg 同梱版を選択してください。
</details>
#### 🐧 Linux
| フォーマット | 説明 |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian パッケージ |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage、ポータブル |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM パッケージ |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak バンドル |
<details>
<summary>🛠️ インストール手順</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # 必要に応じて不足している依存関係を修正
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Flathub の指示に従うか、以下を実行します:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| フォーマット | 説明 |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon 用 ZIP アプリ |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon 用ディスクイメージインストーラー |
<details>
<summary>🛠️ インストール手順</summary>
- **DMG インストーラー (`.dmg`)**: ダブルクリックしてマウントし、`YTSage.app` をアプリケーションフォルダにドラッグします。
- **App アーカイブ (`.zip`)**: ZIP を展開し、`YTSage.app` をアプリケーションフォルダに移動します。
*注意: 「アプリが破損しています」というエラーが表示される場合は、以下の macOS トラブルシューティング セクションを参照してください。*
</details>
---
<details>
<summary>💻 ソースからの手動インストール</summary>
### 1. リポジトリをクローンする
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. 依存関係をインストールする
#### ⚡ uv を使用する場合
```bash
uv pip install .
```
#### 📦 または標準の pip を使用する場合
```bash
pip install .
```
### 3. アプリケーションを実行する
```bash
python -m ytsage.main
```
</details>
<a id="スクリーンショット"></a>
## 📸 スクリーンショット
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="ダウンロード設定" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="プレイリストダウンロード" width="400"/></td>
</tr>
<tr>
<td align="center"><em>ダウンロード設定</em></td>
<td align="center"><em>プレイリストダウンロード</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="オーディオフォーマット選択" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="カスタムオプション" width="400"/></td>
</tr>
<tr>
<td align="center"><em>オーディオフォーマット</em></td>
<td align="center"><em>カスタムオプション</em></td>
</tr>
</table>
</div>
<a id="使い方"></a>
## 📖 使い方
<details>
<summary>🎯 基本的な使い方</summary>
1. **YTSage を起動する**
2. **YouTube の URL を貼り付ける** (または「URL を貼り付け」ボタンを使用)
3. **「分析」をクリックする**
4. **フォーマットを選択する:**
- ビデオダウンロードの場合は `Video`
- オーディオ抽出の場合は `Audio Only`
5. **オプションを選択する:**
- 字幕を有効にして言語を選択
- 字幕結合を有効化
- サムネイルを保存
- スポンサーセクションを削除
- 説明を保存
- チャプターを埋め込む
6. **出力ディレクトリを選択する**
7. **「ダウンロード」をクリックする**
> 💡 デフォルトのダウンロードディレクトリは、ユーザーの「ダウンロード」フォルダです。
</details>
<details>
<summary>📋 プレイリストのダウンロード</summary>
1. **プレイリストの URL を貼り付ける**
2. **「分析」をクリックする**
3. **プレイリストセレクターからビデオを選択する (任意、デフォルトはすべて)**
4. **希望のフォーマット/品質を選択する**
5. **「ダウンロード」をクリックする**
> 💡 アプリケーションはダウンロードキューを自動的に管理し、プレイリストのエントリを `.txt`, `.csv`, `.m3u`, または `.json` ファイルとしてエクスポートできます。
</details>
<details>
<summary>🌍 YouTube 以外のサイト向けのジェネリックモード</summary>
Dailymotion、CBC Gem、TikTok など、yt-dlp がサポートするサイトからの URL を YTSage に受け入れさせたい場合は、ジェネリックモードを使用します。
使用方法:
1. `Download Settings` を開きます。
2. `Generic Mode` を有効にします。
3. YouTube 以外のサポートされているビデオまたはプレイリストの URL を貼り付けます。
4. `Analyze` をクリックします。
5. フォーマットを選択し、通常通りダウンロードします。
注意:
- ジェネリックモードは、YTSage 内部の URL バリデーションのみを変更します。対象サイトは、インストールされている yt-dlp バージョンでサポートされている必要があります。
- サイトによっては、エクストラクターに応じてクッキー、ログイン、プロキシ、または追加の yt-dlp 引数が必要になる場合があります。
- サイトが失敗する場合は、問題を報告する前に統合アップデートタブから yt-dlp を更新してください。
</details>
<details>
<summary>🧰 メディアとダウンロードのオプション</summary>
- **字幕オプション:** 言語をフィルタリングし、字幕をビデオファイルに埋め込みます。
- **字幕結合:** 字幕をビデオファイルにマージして、焼き付け字幕(ハードコード)にします。
- **説明の保存:** ビデオの説明をテキストファイルとして保存します。
- **サムネイルの保存:** ビデオのサムネイルを画像ファイルとして保存します。
- **チャプターの埋め込み:** 対応しているビデオプレーヤー用に、チャプターマーカーをメタデータとして含めます。
- **スポンサーセクションを削除:** SponsorBlock を使用して、ビデオからスポンサーセグメントを削除します。
- **ビデオのトリミング:** `HH:MM:SS` 形式で時間範囲を指定して、ビデオの特定のセクションのみをダウンロードします。
</details>
<details>
<summary>⚙️ 出力とファイルの設定</summary>
- **速度制限:** ダウンロード速度を制限します(例:500 KB/s の場合は `500K`)。
- **ダウンロードパスの保存:** 将来のダウンロードのためにデフォルトのダウンロードパスを保存します。**Download Settings → Download Path** で設定可能です。
- **デフォルトのビデオ解像度:** 自動選択のために好みのビデオ解像度を設定します(例:1080p, 720p)。**Download Settings → Default Video Resolution** で設定可能です。
- **デフォルトの字幕言語:** 自動選択のためにデフォルトの字幕言語を設定します(カンマ区切り、例:`ja,en`)。**Download Settings → Default Subtitle Languages** で設定可能です。
- **ファイル名形式:** `%(title)s`, `%(uploader)s`, `%(playlist_index)s`, `%(resolution)s` などの変数を使用して出力ファイル名の形式をカスタマイズします。**Download Settings → Filename Format** で設定可能です。
- **出力形式の強制:** ビデオダウンロードを `mp4`, `webm`, `mkv` などの特定のコンテナ形式に強制します。**Download Settings → Output Format Settings** で設定可能です。
- **オーディオ形式の変換:** オーディオのみのダウンロードを `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, または `Best` などの好みの形式に変換します。**Download Settings → Audio Format Settings** で設定可能です。
- **オーディオノーマライズ:** EBU R128 を使用して、オーディオのみのダウンロードの音量を標準化します。
- **同時接続数:** ファイルを複数のパーツで同時にダウンロードすることで、ダウンロード速度を大幅に向上させます。**Download Settings → General → Concurrent Connections** で設定可能です(デフォルトは 1。IP ブロックを避けるため最大 8-10 を推奨)。
</details>
<details>
<summary>🌐 アクセスとネットワーク</summary>
- **クッキーログイン:** クッキーを使用して YouTube にログインし、非公開コンテンツにアクセスします。
使用方法:
1. **推奨:** アプリ内蔵の `Extract cookies from browser` オプションを使用し、ブラウザと(必要に応じて)プロフィールを選択します。
2. あるいは、クッキーを手動で抽出します:
a. [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file) などの拡張機能を使用して、ブラウザからクッキーをエクスポートします。
b. Netscape 形式でクッキーをコピーします。
c. `cookies.txt` という名前のファイルを作成し、クッキーを貼り付けます。
d. アプリで `cookies.txt` ファイルを選択します。
- **プロキシサポート:** ダウンロードにプロキシサーバーを使用します(例:`http://<proxy-server>:<port>`)。
- **ジェネリックモード:** YTSage が yt-dlp でサポートされている YouTube 以外のサイトから分析およびダウンロードできるようにします。**Download Settings → Generic Mode** から有効にします。
</details>
<details>
<summary>🛠️ ツールとメンテナンス</summary>
- **カスタムコマンド:** コマンドライン引数を介して高度な yt-dlp 機能にアクセスします。
- **アップデートタブ:** カスタムオプション内の一箇所で内蔵のアップデートツールを管理します:
- **yt-dlp アップデート:** アップデートを確認し、Stable と Nightly リリースチャネルを切り替えます。
- **FFmpeg バージョンチェッカー:** FFmpeg のバージョンを確認し、インストールガイドを開きます。
- **Deno アップデート:** Deno ランタイムを確認し、アップデートします。
- **FFmpeg/yt-dlp/Deno 検出:** About ダイアログから FFmpeg、yt-dlp、Deno のパスとバージョンを自動的に検出します。
- **ダウンロード履歴:** **History** ボタンから、サムネイルとステータス付きで過去のダウンロードを表示します。
</details>
<details>
<summary>🌍 ローカライズ</summary>
YTSage はグローバルに対応するため、**14 言語**をサポートしています。**Custom Options → Language** で好みの言語を選択してください。
### サポートされている言語
| 言語 | コード | 言語 | コード |
|----------|------|----------|------|
| 🇺🇸 英語 | `en` | 🇪🇸 スペイン語 | `es` |
| 🇸🇦 アラビア語 | `ar` | 🇫🇷 フランス語 | `fr` |
| 🇩🇪 ドイツ語 | `de` | 🇮🇳 ヒンディー語 | `hi` |
| 🇮🇩 インドネシア語 | `id` | 🇮🇹 イタリア語 | `it` |
| 🇯🇵 日本語 | `ja` | 🇵🇱 ポーランド語 | `pl` |
| 🇧🇷 ポルトガル語 | `pt` | 🇷🇺 ロシア語 | `ru` |
| 🇹🇷 トルコ語 | `tr` | 🇨🇳 中国語 | `zh` |
### README 翻訳
| 言語 | ファイル | 言語 | ファイル |
|----------|------|----------|------|
| 🇺🇸 英語 | [README.md](../README.md) | 🇪🇸 スペイン語 | [README.es.md](README.es.md) |
| 🇸🇦 アラビア語 | [README.ar.md](README.ar.md) | 🇫🇷 フランス語 | [README.fr.md](README.fr.md) |
| 🇩🇪 ドイツ語 | [README.de.md](README.de.md) | 🇮🇳 ヒンディー語 | [README.hi.md](README.hi.md) |
| 🇮🇩 インドネシア語 | [README.id.md](README.id.md) | 🇮🇹 イタリア語 | [README.it.md](README.it.md) |
| 🇯🇵 日本語 | [README.ja.md](README.ja.md) | 🇵🇱 ポーランド語 | [README.pl.md](README.pl.md) |
| 🇧🇷 ポルトガル語 | [README.pt.md](README.pt.md) | 🇷🇺 ロシア語 | [README.ru.md](README.ru.md) |
| 🇹🇷 トルコ語 | [README.tr.md](README.tr.md) | 🇨🇳 中国語 | [README.zh.md](README.zh.md) |
> 💡 **翻訳を手伝いたいですか?** [貢献](#貢献) セクションを参照して、さらに多くの言語を追加するのを手伝ってください!
</details>
<a id="トラブルシューティング"></a>
## 🛠️ トラブルシューティング
<details>
<summary>クリックして一般的な問題と解決策を表示</summary>
- **フォーマットテーブルが表示されない:** yt-dlp を最新バージョンに更新し、yt-dlp Nightly への切り替えを試してください。
- **ダウンロードが失敗する:** インターネット接続を確認し、ビデオが利用可能であることを確認してください。
- **特定のダウンロードエラー:**
- **非公開ビデオ:** 非公開コンテンツにアクセスするには、クッキー認証を使用してください。
- **年齢制限のあるコンテンツ:** YouTube アカウントにログインして、年齢制限のあるビデオを表示してください。
- **ジオブロックされたビデオ:** 地域制限を回避するために VPN の使用を検討してください。
- **削除されたビデオ:** ビデオは YouTube で利用できなくなっています。
- **ライブストリーム:** ライブストリームは放送中にダウンロードできません。ストリームが終了するまで待ってください。
- **ネットワークエラー:** インターネット接続を確認して再試行してください。
- **無効な URL:** URL が正しく、サポートされているプラットフォームのものであることを確認してください。
- **プレミアムコンテンツ:** YouTube Premium のサブスクリプションが必要です。
- **著作権ブロック:** 著作権制限のため、コンテンツがブロックされています。
- **ダウンロード後にビデオとオーディオファイルが分かれている:** これは FFmpeg が不足しているか、検出されない場合に発生します。YTSage は、高品質のビデオとオーディオストリームを結合するために FFmpeg を必要とします。
- **解決策:** FFmpeg がインストールされており、システムの PATH でアクセス可能であることを確認してください。Windows ユーザーにとって最も簡単なオプションは、FFmpeg が同梱されている `YTSage-v<version>-ffmpeg.exe` ファイルをダウンロードすることです。
---
#### 🛡️ Windows Defender / アンチウイルス警告
一部のアンチウイルスソフトウェアは、`.exe` ファイルを誤検知(False Positive)としてフラグを立てる場合があります。これは、パッケージ化されたアプリケーションの**既知の制限**です。
**なぜ発生するのか:**
- アンチウイルスのヒューリスティック機能が、パッケージ化された実行ファイルを疑わしいものとして誤認することがあります。
**安全な選択肢:**
- ✅ **pip インストールを使用する:** `pip install ytsage` (推奨)
- ✅ **ソースからビルドする**: この[ガイド](../.github/CI_CD_README.md)に従ってください。
- ✅ **アプリケーションをホワイトリストに追加する** (アンチウイルスソフトウェアの設定)。
#### 🍎 macOS: 「アプリが破損しているため開けません」
macOS Sonoma 以降でこのエラーが表示される場合は、 quarantine(隔離)属性を削除する必要があります。
1. **ターミナルを開きます** (Spotlight で検索できます)。
2. **次のコマンドを入力します** が、まだ Enter は押さないでください。最後にスペースを含めるようにしてください:
```bash
xattr -d com.apple.quarantine
```
3. **Finder ウィンドウから `YTSage.app` ファイルをドラッグし**、ターミナルウィンドウに直接ドロップします。これにより、正しいファイルパスが自動的に貼り付けられます。
4. **Enter を押して** コマンドを実行します。
5. **YTSage.app を再度開いてみてください。** 正しく起動するはずです。
---
#### **設定ファイルの場所 (上級者向け)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="スポンサー"></a>
## 💖 スポンサー
YTSage があなたの時間を節約できたなら、プロジェクトのスポンサーになることを検討してください。スポンサーシップは、開発時間、全プラットフォームでのテスト、および将来の改善に役立てられます。
- GitHub Sponsors: https://github.com/sponsors/oop7
- スポンサーリンクは、アプリ内の About ダイアログから直接利用可能です。
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="貢献"></a>
## 👥 貢献
貢献を歓迎します!以下のように手助けができます:
1. 🍴 リポジトリをフォークする
2. 🌿 フィーチャーブランチを作成する:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 変更をコミットする:
```bash
git commit -m 'Add some AmazingFeature'
```
4. 📤 ブランチにプッシュする:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 プルリクエストを作成する
### 🌍 翻訳に貢献する
- 関連するローカライズ版 README ファイルを更新する (例: `readme-translations/README.ja.md`)
- `ytsage/languages/<code>.json` を編集して、アプリの文字列を同期させる
- お使いの言語がない場合は、 `README.md` をベースに `README.<code>.md` を作成してください。
<details>
<summary>📂 プロジェクト構造</summary>
## YTSage - プロジェクト構造
このドキュメントでは、YTSage の整理されたフォルダ構造について説明します。
### 📁 プロジェクト構造
```
YTSage/
├── 📁 .github/ # GitHub 設定
│ ├── 📁 ISSUE_TEMPLATE/ # イシューテンプレート
│ │ └── 🐛-bug-report.md # バグレポートテンプレート
│ ├─── 📁 workflows/ # GitHub Actions ワークフロー
│ │ ├── build-linux.yml # Linux ビルドワークフロー
│ │ ├── build-macos.yml # macOS ビルドワークフロー
│ │ │── build-windows.yml # Windows ビルドワークフロー
| | └── release-all.yml # マスターリリースワークフロー
│ └── 📄 CI_CD_README.md # CI/CD ドキュメント
├── 📁 branding/ # ブランディングアセット (スクリーンショット, SVG)
│ ├── 📁 icons/ # アプリアイコン
│ ├── 📁 screenshots/ # ドキュメント用スクリーンショット
│ └── 📁 svg/ # SVG アセット
├── 📄 LICENSE # ライセンスファイル
├── 📄 pyproject.toml # プロジェクトメタデータと依存関係
├── 📄 README.md # プロジェクトドキュメント
├── 📄 requirements.txt # Python 依存関係 (dev)
└── 📁 ytsage/ # ソースコードパッケージ
├── 📁 assets/ # ランタイムアセット
│ ├── 📁 Icon/ # アプリアイコン
│ └── 📁 sound/ # オーディオファイル
├── 📁 languages/ # ローカライズファイル
│ ├── 📄 ar.json # アラビア語翻訳
│ ├── 📄 de.json # ドイツ語翻訳
│ ├── 📄 en.json # 英語翻訳
│ └── ... # その他の言語
├── 📁 core/ # コアビジネスロジック
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Deno 統合
│ ├── 📄 ytsage_downloader.py # ダウンロード機能
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg 統合
│ ├── 📄 ytsage_utils.py # ユーティリティ関数
│ └── 📄 ytsage_yt_dlp.py # yt-dlp 統合
├── 📁 gui/ # ユーザーインターフェースコンポーネント
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # アプリメインウィンドウ
│ └── 📁 ytsage_gui_dialogs/ # ダイアログクラス
├── 📁 utils/ # ユーティリティモジュール
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # 設定管理
│ └── 📄 ytsage_logger.py # ロギングツール
├── 📄 __init__.py # パッケージエントリポイント
└── 📄 main.py # メイン実行スクリプト
```
</details>
## ⭐️ スター履歴
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 ライセンス
このプロジェクトは MIT ライセンスの下でライセンスされています。詳細は [LICENSE](../LICENSE) ファイルを参照してください。
## 🙏 謝辞
<details>
<summary>謝辞を表示</summary>
<div align="center">
<p>改善の提案やバグの報告のためにイシューを開いてこのプロジェクトに貢献してくださったすべての方々に感謝いたします。</p>
<table>
<tr class="section"><th colspan="2">主要コンポーネント</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>ダウンロードエンジン</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>メディア処理</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>yt-dlp 統合用ランタイム</td>
</tr>
<tr class="section"><th colspan="2">ライブラリとフレームワーク</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI フレームワーク</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>画像処理</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>HTTP リクエスト</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>バージョン管理とパッケージ化</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown レンダリング</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>ロギング</td>
</tr>
<tr class="section"><th colspan="2">アセットと貢献者</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>通知音</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>コード貢献者</td>
</tr>
</table>
</div>
</details>
## ⚠️ 免責事項
このツールは個人利用のみを目的としています。YouTube の利用規約およびコンテンツ制作者の権利を尊重してください。
---
<div align="center">
Created with ❤️ by [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Nowoczesny downloader YouTube z czystym interfejsem PySide6.**
Pobieraj wideo w dowolnej jakości, wyodrębniaj audio, pobieraj napisy i wiele więcej.
### 🌍 Języki README
Angielski: [EN](../README.md)
| Arabski: [AR](README.ar.md)
| Niemiecki: [DE](README.de.md)
| Hiszpański: [ES](README.es.md)
| Francuski: [FR](README.fr.md)
| Hindi: [HI](README.hi.md)
| Indonezyjski: [ID](README.id.md)
| Włoski: [IT](README.it.md)
| Japoński: [JA](README.ja.md)
| Polski: [PL](README.pl.md)
| Portugalski: [PT](README.pt.md)
| Rosyjski: [RU](README.ru.md)
| Turecki: [TR](README.tr.md)
| Chiński: [ZH](README.zh.md)
<p align="center">
<a href="#instalacja">Instalacja</a> •
<a href="#funkcje">Funkcje</a> •
<a href="#użycie">Użycie</a> •
<a href="#zrzuty-ekranu">Zrzuty ekranu</a> •
<a href="#rozwiązywanie-problemów">Rozwiązywanie problemów</a> •
<a href="#sponsor">Sponsor</a> •
<a href="#współpraca">Współpraca</a>
</p>
</div>
---
<a id="dlaczego-ytsage"></a>
## ❓ Dlaczego YTSage?
YTSage został zaprojektowany dla użytkowników, którzy chcą **prostego, ale potężnego downloadera YouTube**. W przeciwieństwie do innych narzędzi oferuje:
- Nowoczesny i czysty interfejs PySide6
- Pobieranie wideo, audio i napisów jednym kliknięciem
- Zaawansowane funkcje, takie jak SponsorBlock, scalanie napisów i wybór playlisty
- Opcjonalny tryb ogólny (Generic Mode) dla stron spoza YouTube obsługiwanych przez yt-dlp
- Obsługa wielu platform i łatwa instalacja
<a id="funkcje"></a>
## ✨ Funkcje
<div align="center">
| Funkcje podstawowe | Funkcje zaawansowane | Funkcje dodatkowe |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Tabela formatów | 🚫 Integracja SponsorBlock | 🎞️ Wyświetlanie FPS/HDR |
| 🎵 Wyodrębnianie audio | 📝 Wybór i scalanie napisów | 🔄 Auto-aktualizacja yt-dlp |
| ✨ Prosty interfejs użytkownika | 💾 Zapis opisu i miniatur | 🛠️ Wykrywanie FFmpeg/yt-dlp/Deno |
| 📋 Obsługa i wybór playlist | 🚀 Ogranicznik prędkości | ⚙️ Własne komendy |
| 📑 Integracja rozdziałów | ✂️ Przycinanie sekcji wideo | 🍪 Logowanie przez ciasteczka |
| 📜 Historia pobierania | 🔄 Wybór kanału wydań | 🌐 Obsługa proxy |
| 🎚️ Konwersja formatów audio | 🎬 Ustawienia formatu wideo | 🆙 Zintegrowana karta aktualizacji |
| 🌍 Tryb ogólny | 🔊 Normalizacja audio (EBU R128) | 🌍 Lokalizacja w 14 językach |
| 💾 Eksport playlisty | ⚙️ Domyślna jakość i napisy | |
</div>
<a id="instalacja"></a>
## 🚀 Instalacja
### ⚡ Szybka instalacja (zalecana)
Zainstaluj YTSage przez PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Aktualizacja istniejącej instalacji</summary>
```bash
pip install --upgrade ytsage
```
</details>
Następnie uruchom aplikację:
```bash
ytsage
```
### 📦 Gotowe pliki wykonywalne (Executable)
> [👉 Pobierz najnowsze wydanie](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Format | Opis |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Standardowy instalator |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Z dołączonym FFmpeg |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Wersja przenośna, nie wymaga instalacji |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Przenośna z FFmpeg, skompresowana (ZIP) |
<details>
<summary>🛠️ Kroki instalacji</summary>
1. **Instalator EXE (`.exe`)**: Kliknij dwukrotnie plik i postępuj zgodnie z instrukcjami kreatora.
2. **Wersja przenośna (`.zip`)**: Rozpakuj archiwum w wybranym miejscu i uruchom `ytsage.exe`.
3. **Wbudowany FFmpeg**: Jeśli nie masz zainstalowanego FFmpeg w systemie, wybierz wersję z dołączonym FFmpeg.
</details>
#### 🐧 Linux
| Format | Opis |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Pakiet Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, przenośny |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Pakiet RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Pakiet Flatpak |
<details>
<summary>🛠️ Kroki instalacji</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Jeśli trzeba naprawić brakujące zależności
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Postępuj zgodnie z instrukcjami na Flathub lub uruchom:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Format | Opis |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Aplikacja ZIP dla Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Instalator Disk Image dla Apple Silicon |
<details>
<summary>🛠️ Kroki instalacji</summary>
- **Instalator DMG (`.dmg`)**: Kliknij dwukrotnie, aby zamontować, a następnie przeciągnij `YTSage.app` do folderu Aplikacje.
- **Archiwum aplikacji (`.zip`)**: Rozpakuj ZIP i przenieś `YTSage.app` do folderu Aplikacje.
*Uwaga: Jeśli otrzymasz błąd "Aplikacja jest uszkodzona", zapoznaj się z sekcją rozwiązywania problemów macOS poniżej.*
</details>
---
<details>
<summary>💻 Ręczna instalacja ze źródeł</summary>
### 1. Sklonuj repozytorium
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Zainstaluj zależności
#### ⚡ Z użyciem uv
```bash
uv pip install .
```
#### 📦 Lub ze standardowym pip
```bash
pip install .
```
### 3. Uruchom aplikację
```bash
python -m ytsage.main
```
</details>
<a id="zrzuty-ekranu"></a>
## 📸 Zrzuty ekranu
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Ustawienia pobierania" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Pobieranie playlisty" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Ustawienia pobierania</em></td>
<td align="center"><em>Pobieranie playlisty</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Wybór formatu audio" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Opcje niestandardowe" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Format audio</em></td>
<td align="center"><em>Opcje niestandardowe</em></td>
</tr>
</table>
</div>
<a id="użycie"></a>
## 📖 Użycie
<details>
<summary>🎯 Podstawowe użycie</summary>
1. **Uruchom YTSage**
2. **Wklej URL z YouTube** (lub użyj przycisku "Wklej URL")
3. **Kliknij "Analizuj"**
4. **Wybierz format:**
- `Wideo` dla pobierania wideo
- `Tylko audio` dla wyodrębniania dźwięku
5. **Wybierz opcje:**
- Włącz napisy i wybierz język
- Włącz scalanie napisów
- Zapisz miniaturę
- Usuń sekcje sponsorowane
- Zapisz opis
- Osadź rozdziały
6. **Wybierz folder wyjściowy**
7. **Kliknij "Pobierz"**
> 💡 Domyślny folder pobierania to folder "Pobrane" użytkownika.
</details>
<details>
<summary>📋 Pobieranie playlisty</summary>
1. **Wklej URL playlisty**
2. **Kliknij "Analizuj"**
3. **Wybierz filmy z selektora (opcjonalnie, domyślnie wszystkie)**
4. **Wybierz żądany format/jakość**
5. **Kliknij "Pobierz"**
> 💡 Aplikacja automatycznie zarządza kolejką pobierania, a wpisy playlisty możesz eksportować do plików `.txt`, `.csv`, `.m3u` lub `.json`.
</details>
<details>
<summary>🌍 Tryb ogólny dla stron innych niż YouTube</summary>
Użyj trybu ogólnego (Generic Mode), gdy chcesz, aby YTSage akceptował adresy URL ze stron obsługiwanych przez yt-dlp, takich jak Dailymotion, CBC Gem, TikTok i inne.
Jak go użyć:
1. Otwórz `Download Settings`.
2. Włącz `Generic Mode`.
3. Wklej obsługiwany URL wideo lub playlisty spoza YouTube.
4. Kliknij `Analyze`.
5. Wybierz format i pobierz jak zwykle.
Uwagi:
- Tryb ogólny zmienia tylko walidację adresu URL wewnątrz YTSage. Strona docelowa musi być nadal obsługiwana przez zainstalowaną wersję yt-dlp.
- Niektóre strony wymagają ciasteczek, logowania, proxy lub dodatkowych argumentów yt-dlp w zależności od ekstraktora.
- Jeśli strona nie działa, zaktualizuj yt-dlp ze zintegrowanej karty aktualizacji przed zgłoszeniem problemu.
</details>
<details>
<summary>🧰 Opcje mediów i pobierania</summary>
- **Opcje napisów:** Filtruj języki i osadzaj napisy w pliku wideo.
- **Scalanie napisów:** Scala napisy z plikiem wideo (hardcoded).
- **Zapis opisu:** Zapisuje opis wideo jako plik tekstowy.
- **Zapis miniatury:** Zapisuje miniaturę wideo jako plik graficzny.
- **Osadź rozdziały:** Dołącza znaczniki rozdziałów jako metadane dla kompatybilnych odtwarzaczy.
- **Usuń sekcje sponsorowane:** Używa SponsorBlock do usuwania segmentów sponsorowanych z filmu.
- **Przycinanie wideo:** Pobieraj tylko określone części filmu, określając zakres czasu w formacie `GG:MM:SS`.
</details>
<details>
<summary>⚙️ Ustawienia wyjściowe i plików</summary>
- **Ogranicznik prędkości:** Ogranicz prędkość pobierania, np. `500K` dla 500 KB/s.
- **Zapisz ścieżkę pobierania:** Zapisuje domyślną ścieżkę dla przyszłych pobrań. Dostępne w **Download Settings → Download Path**.
- **Domyślna rozdzielczość wideo:** Ustaw preferowaną rozdzielczość dla automatycznego wyboru (np. 1080p, 720p). Dostępne w **Download Settings → Default Video Resolution**.
- **Domyślne języki napisów:** Ustaw domyślne języki dla automatycznego wyboru (rozdzielone przecinkami, np. `pl,en`). Dostępne w **Download Settings → Default Subtitle Languages**.
- **Format nazwy pliku:** Dostosuj format nazwy pliku wyjściowego, używając zmiennych takich jak `%(title)s`, `%(uploader)s`, `%(playlist_index)s` i `%(resolution)s`. Dostępne w **Download Settings → Filename Format**.
- **Wymuś format wyjściowy:** Wymuś pobieranie wideo do konkretnego formatu kontenera, jak `mp4`, `webm` lub `mkv`. Dostępne w **Download Settings → Output Format Settings**.
- **Konwersja formatów audio:** Konwertuj pobierany dźwięk do preferowanego formatu, takiego jak `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis` lub `Best`. Dostępne w **Download Settings → Audio Format Settings**.
- **Normalizacja audio:** Ujednolica głośność pobieranego dźwięku przy użyciu EBU R128.
- **Jednoczesne połączenia:** Znacznie zwiększa prędkość pobierania, pobierając pliki w kilku częściach naraz. Dostępne w **Download Settings → General → Concurrent Connections** (domyślnie 1, zalecane maks. 8-10, aby uniknąć blokad IP).
</details>
<details>
<summary>🌐 Dostęp i sieć</summary>
- **Logowanie przez ciasteczka:** Zaloguj się do YouTube, używając ciasteczek, aby uzyskać dostęp do prywatnych treści.
Użycie:
1. **Zalecane:** Użyj wbudowanej opcji `Eksportuj ciasteczka z przeglądarki`, a następnie wybierz przeglądarkę i opcjonalnie profil.
2. Alternatywnie, wyeksportuj ciasteczka ręcznie:
a. Eksportuj ciasteczka z przeglądarki, używając rozszerzenia typu [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Skopiuj ciasteczka w formacie Netscape
c. Utwórz plik o nazwie `cookies.txt` i wklej ciasteczka
d. Wybierz plik `cookies.txt` w aplikacji
- **Obsługa proxy:** Użyj serwera proxy do pobierania, np. `http://<serwer-proxy>:<port>`
- **Tryb ogólny:** Pozwala YTSage na analizę i pobieranie z witryn innych niż YouTube obsługiwanych przez yt-dlp. Włącz w **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Narzędzia i konserwacja</summary>
- **Własne komendy:** Dostęp do zaawansowanych funkcji yt-dlp poprzez argumenty wiersza poleceń.
- **Karta aktualizacji:** Zarządzaj wbudowanymi narzędziami aktualizacji z jednego miejsca w opcjach niestandardowych:
- **Aktualizacja yt-dlp:** Sprawdzaj aktualizacje i przełączaj się między kanałami Stable i Nightly.
- **Sprawdzanie wersji FFmpeg:** Weryfikuj wersję FFmpeg i otwieraj przewodniki instalacji.
- **Aktualizacja Deno:** Sprawdzaj i aktualizuj środowisko uruchomieniowe Deno.
- **Wykrywanie FFmpeg/yt-dlp/Deno:** Automatycznie wykrywa ścieżki i wersje FFmpeg, yt-dlp i Deno w oknie "O programie".
- **Historia pobierania:** Przeglądaj poprzednie pobrania z miniaturami i statusami za pomocą przycisku **History**.
</details>
<details>
<summary>🌍 Lokalizacja</summary>
YTSage obsługuje **14 języków**. Wybierz preferowany język w **Custom Options → Language**.
### Obsługiwane języki
| Język | Kod | Język | Kod |
|----------|------|----------|------|
| 🇺🇸 Angielski | `en` | 🇪🇸 Hiszpański | `es` |
| 🇸🇦 Arabski | `ar` | 🇫🇷 Francuski | `fr` |
| 🇩🇪 Niemiecki | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonezyjski | `id` | 🇮🇹 Włoski | `it` |
| 🇯🇵 Japoński | `ja` | 🇵🇱 Polski | `pl` |
| 🇧🇷 Portugalski | `pt` | 🇷🇺 Rosyjski | `ru` |
| 🇹🇷 Turecki | `tr` | 🇨🇳 Chiński | `zh` |
### Tłumaczenia README
| Język | Plik | Język | Plik |
|----------|------|----------|------|
| 🇺🇸 Angielski | [README.md](../README.md) | 🇪🇸 Hiszpański | [README.es.md](README.es.md) |
| 🇸🇦 Arabski | [README.ar.md](README.ar.md) | 🇫🇷 Francuski | [README.fr.md](README.fr.md) |
| 🇩🇪 Niemiecki | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonezyjski | [README.id.md](README.id.md) | 🇮🇹 Włoski | [README.it.md](README.it.md) |
| 🇯🇵 Japoński | [README.ja.md](README.ja.md) | 🇵🇱 Polski | [README.pl.md](README.pl.md) |
| 🇧🇷 Portugalski | [README.pt.md](README.pt.md) | 🇷🇺 Rosyjski | [README.ru.md](README.ru.md) |
| 🇹🇷 Turecki | [README.tr.md](README.tr.md) | 🇨🇳 Chiński | [README.zh.md](README.zh.md) |
> 💡 **Chcesz pomóc w tłumaczeniu?** Sprawdź sekcję [Współpraca](#współpraca), aby pomóc nam dodać więcej języków!
</details>
<a id="rozwiązywanie-problemów"></a>
## 🛠️ Rozwiązywanie problemów
<details>
<summary>Kliknij, aby zobaczyć typowe problemy i rozwiązania</summary>
- **Tabela formatów nie pojawia się:** Zaktualizuj yt-dlp do najnowszej wersji i spróbuj przełączyć się na kanał yt-dlp Nightly.
- **Pobieranie nie powiodło się:** Sprawdź połączenie z internetem i upewnij się, że wideo jest dostępne.
- **Konkretne błędy pobierania:**
- **Prywatne wideo:** Użyj uwierzytelniania przez ciasteczka, aby uzyskać dostęp do prywatnych treści.
- **Treści z ograniczeniem wiekowym:** Zaloguj się na swoje konto YouTube, aby wyświetlić filmy z ograniczeniem wiekowym.
- **Wideo z blokadą regionalną:** Rozważ użycie VPN, aby obejść ograniczenia regionalne.
- **Usunięte wideo:** Film nie jest już dostępny na YouTube.
- **Transmisje na żywo:** Transmisji na żywo nie można pobierać w trakcie ich trwania; poczekaj, aż transmisja się zakończy.
- **Błędy sieciowe:** Sprawdź połączenie z internetem i spróbuj ponownie.
- **Nieprawidłowy URL:** Upewnij się, że adres URL jest poprawny i pochodzi z obsługiwanej platformy.
- **Treść Premium:** Wymaga subskrypcji YouTube Premium.
- **Blokada praw autorskich:** Treść jest zablokowana z powodu ograniczeń praw autorskich.
- **Pliki wideo i audio są oddzielne po pobraniu:** Dzieje się tak, gdy brakuje FFmpeg lub nie został on wykryty. YTSage wymaga FFmpeg do scalania strumieni wideo i audio wysokiej jakości.
- **Rozwiązanie:** Upewnij się, że FFmpeg jest zainstalowany i dostępny w zmiennej PATH systemu. Dla użytkowników Windows najprostszą opcją jest pobranie pliku `YTSage-v<wersja>-ffmpeg.exe`, który zawiera FFmpeg.
---
#### 🛡️ Ostrzeżenie Windows Defender / Antywirus
Niektóre programy antywirusowe mogą oznaczać pliki `.exe` jako fałszywe alarmy (false positive). Jest to **znane ograniczenie** pakowanych aplikacji.
**Dlaczego tak się dzieje:**
- Heurystyka antywirusa może błędnie zidentyfikować spakowane pliki wykonywalne jako podejrzane.
**Bezpieczne alternatywy:**
- ✅ **Użyj instalacji przez pip:** `pip install ytsage` (zalecane)
- ✅ **Zbuduj ze źródeł**: Postępując zgodnie z tym [przewodnikiem](../.github/CI_CD_README.md)
- ✅ **Dodaj aplikację do wyjątków** w swoim programie antywirusowym.
#### 🍎 macOS: "Aplikacja jest uszkodzona i nie można jej otworzyć"
Jeśli widzisz ten błąd w systemie macOS Sonoma lub nowszym, musisz usunąć atrybut kwarantanny.
1. **Otwórz Terminal** (możesz go znaleźć przez Spotlight).
2. **Wpisz następującą komendę**, ale jeszcze **NIE** naciskaj Enter. Pamiętaj o spacjach na końcu:
```bash
xattr -d com.apple.quarantine
```
3. **Przeciągnij plik `YTSage.app` z okna Findera** i upuść go bezpośrednio w oknie Terminala. To automatycznie wklei poprawną ścieżkę do pliku.
4. **Naciśnij Enter**, aby uruchomić komendę.
5. **Spróbuj ponownie otworzyć YTSage.app.** Powinien uruchomić się poprawnie.
---
#### **Lokalizacja konfiguracji (zaawansowane)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor"></a>
## 💖 Sponsor
Jeśli YTSage oszczędza Twój czas, rozważ wsparcie projektu. Sponsoring pomaga pokryć czas programistyczny, testy na wszystkich platformach i przyszłe ulepszenia.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Link do wsparcia jest dostępny bezpośrednio przez okno "O programie" w aplikacji.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="współpraca"></a>
## 👥 Współpraca
Zapraszamy do współpracy! Oto jak możesz pomóc:
1. 🍴 Skonfiguruj fork repozytorium
2. 🌿 Utwórz gałąź (branch) dla swojej funkcji:
```bash
git checkout -b feature/NiesamowitaFunkcja
```
3. 💾 Zatwierdź zmiany (commit):
```bash
git commit -m 'Dodaj NiesamowitaFunkcja'
```
4. 📤 Wyślij zmiany do gałęzi (push):
```bash
git push origin feature/NiesamowitaFunkcja
```
5. 🔄 Otwórz Pull Request
### 🌍 Pomóż w tłumaczeniach
- Zaktualizuj odpowiedni lokalny plik README (np. `readme-translations/README.pl.md`)
- Dbaj o synchronizację komunikatów aplikacji, edytując `ytsage/languages/<code>.json`
- Jeśli brakuje Twojego języka, zacznij od `README.md` i stwórz `README.<code>.md`
<details>
<summary>📂 Struktura projektu</summary>
## YTSage - Struktura projektu
Ten dokument opisuje zorganizowaną strukturę folderów projektu YTSage.
### 📁 Struktura projektu
```
YTSage/
├── 📁 .github/ # Konfiguracja GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Szablony zgłoszeń
│ │ └── 🐛-bug-report.md # Szablon zgłoszenia błędu
│ ├─── 📁 workflows/ # Procesy GitHub Actions
│ │ ├── build-linux.yml # Proces budowania dla Linux
│ │ ├── build-macos.yml # Proces budowania dla macOS
│ │ │── build-windows.yml # Proces budowania dla Windows
| | └── release-all.yml # Proces głównego wydania
│ └── 📄 CI_CD_README.md # Dokumentacja CI/CD
├── 📁 branding/ # Materiały graficzne (zrzuty ekranu, SVG)
│ ├── 📁 icons/ # Ikony aplikacji
│ ├── 📁 screenshots/ # Zrzuty ekranu do dokumentacji
│ └── 📁 svg/ # Zasoby SVG
├── 📄 LICENSE # Plik licencji
├── 📄 pyproject.toml # Metadane projektu i zależności
├── 📄 README.md # Dokumentacja projektu
├── 📄 requirements.txt # Zależności Python (dev)
└── 📁 ytsage/ # Pakiet kodu źródłowego
├── 📁 assets/ # Zasoby czasu wykonywania
│ ├── 📁 Icon/ # Ikony aplikacji
│ └── 📁 sound/ # Pliki dźwiękowe
├── 📁 languages/ # Pliki lokalizacji
│ ├── 📄 ar.json # Tłumaczenie arabskie
│ ├── 📄 de.json # Tłumaczenie niemieckie
│ ├── 📄 en.json # Tłumaczenie angielskie
│ └── ... # Inne języki
├── 📁 core/ # Główna logika biznesowa
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Integracja Deno
│ ├── 📄 ytsage_downloader.py # Funkcjonalność pobierania
│ ├── 📄 ytsage_ffmpeg.py # Integracja FFmpeg
│ ├── 📄 ytsage_utils.py # Funkcje pomocnicze
│ └── 📄 ytsage_yt_dlp.py # Integrasi yt-dlp
├── 📁 gui/ # Komponenty interfejsu użytkownika
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # Główne okno aplikacji
│ └── 📁 ytsage_gui_dialogs/ # Klasy okien dialogowych
├── 📁 utils/ # Moduły pomocnicze
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # Zarządzanie konfiguracją
│ └── 📄 ytsage_logger.py # Narzędzia logowania
├── 📄 __init__.py # Punkt wejścia pakietu
└── 📄 main.py # Główny skrypt wykonawczy
```
</details>
## ⭐️ Historia gwiazdek
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Licencja
Ten projekt jest udostępniany na licencji MIT - zobacz plik [LICENSE](../LICENSE), aby poznać szczegóły.
## 🙏 Podziękowania
<details>
<summary>Pokaż podziękowania</summary>
<div align="center">
<p>Wielkie podziękowania dla wszystkich, którzy przyczynili się do rozwoju projektu poprzez zgłaszanie sugestii i błędów.</p>
<table>
<tr class="section"><th colspan="2">Główne komponenty</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Silnik pobierania</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Przetwarzanie mediów</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Środowisko dla integracji yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Biblioteki i frameworki</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>Framework GUI</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Przetwarzanie obrazów</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>Żądania HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Zarządzanie wersjami i pakowanie</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Renderowanie Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Logowanie</td>
</tr>
<tr class="section"><th colspan="2">Zasoby i współpracownicy</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 by Universfield</a></td>
<td>Dźwięk powiadomienia</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Współpracownik kodu</td>
</tr>
</table>
</div>
</details>
## ⚠️ Zastrzeżenie
To narzędzie służy wyłącznie do użytku osobistego. Prosimy o przestrzeganie Warunków świadczenia usług YouTube i praw twórców treści.
---
<div align="center">
Stworzone z ❤️ przez [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Interface"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Um downloader de YouTube moderno com uma interface PySide6 limpa.**
Baixe vídeos em qualquer qualidade, extraia áudio, obtenha legendas e muito mais.
### 🌍 Idiomas do README
Inglês: [EN](../README.md)
| Árabe: [AR](README.ar.md)
| Alemão: [DE](README.de.md)
| Espanhol: [ES](README.es.md)
| Francês: [FR](README.fr.md)
| Hindi: [HI](README.hi.md)
| Indonésio: [ID](README.id.md)
| Italiano: [IT](README.it.md)
| Japonês: [JA](README.ja.md)
| Polonês: [PL](README.pl.md)
| Português: [PT](README.pt.md)
| Russo: [RU](README.ru.md)
| Turco: [TR](README.tr.md)
| Chinês: [ZH](README.zh.md)
<p align="center">
<a href="#instalação">Instalação</a> •
<a href="#funcionalidades">Funcionalidades</a> •
<a href="#uso">Uso</a> •
<a href="#capturas-de-tela">Capturas de Tela</a> •
<a href="#solução-de-problemas">Solução de Problemas</a> •
<a href="#patrocinar">Patrocinar</a> •
<a href="#contribuindo">Contribuindo</a>
</p>
</div>
---
<a id="por-que-ytsage"></a>
## ❓ Por que YTSage?
O YTSage foi projetado para usuários que desejam um **downloader de YouTube simples, mas poderoso**. Ao contrário de outras ferramentas, ele oferece:
- Uma interface PySide6 moderna e limpa
- Download de vídeo, áudio e legendas com um clique
- Recursos avançados como SponsorBlock, mesclagem de legendas e seleção de playlist
- Modo Genérico opcional para sites além do YouTube suportados pelo yt-dlp
- Suporte multiplataforma e instalação fácil
<a id="funcionalidades"></a>
## ✨ Funcionalidades
<div align="center">
| Recursos Básicos | Recursos Avançados | Recursos Extras |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Tabela de Formatos | 🚫 Integração SponsorBlock | 🎞️ Exibição de FPS/HDR |
| 🎵 Extração de Áudio | 📝 Seleção e Mesclagem de Legendas | 🔄 Atualização Automática do yt-dlp |
| ✨ Interface de Usuário Simples | 💾 Salvar Descrição e Miniatura | 🛠️ Detecção de FFmpeg/yt-dlp/Deno |
| 📋 Suporte e Seletor de Playlist | 🚀 Limitador de Velocidade | ⚙️ Comandos Personalizados |
| 📑 Integração de Capítulos | ✂️ Cortar Seções de Vídeo | 🍪 Login por Cookies |
| 📜 Histórico de Downloads | 🔄 Escolha do Canal de Lançamento | 🌐 Suporte a Proxy |
| 🎚️ Conversão de Formato de Áudio | 🎬 Configurações de Formato de Vídeo | 🆙 Aba de Atualização Integrada |
| 🌍 Modo Genérico | 🔊 Normalização de Áudio (EBU R128) | 🌍 Localização em 14 idiomas |
| 💾 Exportação de Playlist | ⚙️ Qualidade e Legendas Padrão | |
</div>
<a id="instalação"></a>
## 🚀 Instalação
### ⚡ Instalação Rápida (Recomendada)
Instale o YTSage via PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Atualizar Instalação Existente</summary>
```bash
pip install --upgrade ytsage
```
</details>
Em seguida, execute o aplicativo:
```bash
ytsage
```
### 📦 Executáveis Pré-compilados (Executable)
> [👉 Baixar Lançamento Mais Recente](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Formato | Descrição |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Instalador Padrão |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Com FFmpeg incluído |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Versão Portátil, sem necessidade de instalação |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Portátil com FFmpeg, compactado (ZIP) |
<details>
<summary>🛠️ Passos para Instalação</summary>
1. **Instalador EXE (`.exe`)**: Clique duas vezes no arquivo e siga o assistente de configuração.
2. **Versão Portátil (`.zip`)**: Extraia o arquivo para o local desejado e execute `ytsage.exe`.
3. **FFmpeg Integrado**: Se você não possui o FFmpeg instalado no sistema, escolha as versões com FFmpeg integrado.
</details>
#### 🐧 Linux
| Formato | Descrição |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Pacote Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, Portátil |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Pacote RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Pacote Flatpak |
<details>
<summary>🛠️ Passos para Instalação</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Se necessário para corrigir dependências ausentes
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Siga as instruções no Flathub ou execute:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Formato | Descrição |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Aplicativo ZIP para Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Instalador Disk Image para Apple Silicon |
<details>
<summary>🛠️ Passos para Instalação</summary>
- **Instalador DMG (`.dmg`)**: Clique duas vezes para montar e arraste `YTSage.app` para a sua pasta Aplicativos.
- **Arquivo do Aplicativo (`.zip`)**: Extraia o ZIP e mova `YTSage.app` para a sua pasta Aplicativos.
*Nota: Se você receber o erro "App está danificado", veja a seção de Problemos no macOS abaixo.*
</details>
---
<details>
<summary>💻 Instalação Manual a partir do Código-Fonte</summary>
### 1. Clonar o Repositório
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Instalar Dependências
#### ⚡ Com uv
```bash
uv pip install .
```
#### 📦 Ou com pip padrão
```bash
pip install .
```
### 3. Executar o Aplicativo
```bash
python -m ytsage.main
```
</details>
<a id="capturas-de-tela"></a>
## 📸 Capturas de Tela
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Configurações de Download" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Download de Playlist" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Configurações de Download</em></td>
<td align="center"><em>Download de Playlist</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Seleção de Formato de Áudio" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Opções Personalizadas" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Formato de Áudio</em></td>
<td align="center"><em>Opções Personalizadas</em></td>
</tr>
</table>
</div>
<a id="uso"></a>
## 📖 Uso
<details>
<summary>🎯 Uso Básico</summary>
1. **Inicie o YTSage**
2. **Cole uma URL do YouTube** (ou use o botão "Paste URL")
3. **Clique em "Analyze"**
4. **Escolha o Formato:**
- `Video` para download de vídeo
- `Audio Only` para extração de áudio
5. **Selecione Opções:**
- Habilite legendas e escolha o idioma
- Habilite a mesclagem de legendas
- Salvar miniatura
- Remover seções de patrocinadores
- Salvar descrição
- Incorporar capítulos
6. **Escolha o Diretório de Saída**
7. **Clique em "Download"**
> 💡 O diretório de download padrão é a pasta "Downloads" do usuário.
</details>
<details>
<summary>📋 Download de Playlist</summary>
1. **Cole a URL da Playlist**
2. **Clique em "Analyze"**
3. **Selecione vídeos do seletor (opcional, padrão todos)**
4. **Escolha o formato/qualidade desejado**
5. **Clique em "Download"**
> 💡 O aplicativo gerencia automaticamente a fila de download, e você pode exportar as entradas da playlist como arquivos `.txt`, `.csv`, `.m3u` ou `.json`.
</details>
<details>
<summary>🌍 Modo Genérico para Sites além do YouTube</summary>
Use o Modo Genérico quando quiser que o YTSage aceite URLs de sites suportados pelo yt-dlp, como Dailymotion, CBC Gem, TikTok e outros.
Como usar:
1. Abra `Download Settings`.
2. Habilite `Generic Mode`.
3. Cole uma URL de vídeo ou playlist suportada que não seja do YouTube.
4. Clique em `Analyze`.
5. Escolha um formato e baixe normalmente.
Notas:
- O Modo Genérico apenas altera a validação da URL dentro do YTSage. O site de destino ainda deve ser suportado pela sua versão instalada do yt-dlp.
- Alguns sites exigem cookies, login, proxy ou argumentos adicionais do yt-dlp, dependendo do extrator.
- Se um site falhar, atualize o yt-dlp na aba de atualização integrada antes de relatar o problema.
</details>
<details>
<summary>🧰 Opções de Mídia e Download</summary>
- **Opções de Legendas:** Filtre idiomas e incorpore legendas no arquivo de vídeo.
- **Mesclagem de Legendas:** Mescla legendas no arquivo de vídeo para legendas fixas (hardcoded).
- **Salvar Descrição:** Salva a descrição do vídeo como um arquivo de texto.
- **Salvar Miniatura:** Salva a miniatura do vídeo como um arquivo de imagem.
- **Incorporar Capítulos:** Inclui marcadores de capítulo como metadados para players de vídeo compatíveis.
- **Remover Seções de Patrocinadores:** Usa o SponsorBlock para remover segmentos patrocinados do vídeo.
- **Cortar Vídeo:** Baixe apenas partes específicas do vídeo, especificando o intervalo de tempo no formato `HH:MM:SS`.
</details>
<details>
<summary>⚙️ Configurações de Saída e Arquivos</summary>
- **Limitador de Velocidade:** Limite a velocidade de download, por exemplo, `500K` para 500 KB/s.
- **Salvar Caminho de Download:** Salva o caminho de download padrão para downloads futuros. Disponível em **Download Settings → Download Path**.
- **Resolução de Vídeo Padrão:** Defina sua resolução de vídeo preferida para seleção automática (ex: 1080p, 720p). Disponível em **Download Settings → Default Video Resolution**.
- **Idiomas de Legendas Padrão:** Defina idiomas de legendas padrão para seleção automática (separados por vírgula, ex: `pt,en`). Disponível em **Download Settings → Default Subtitle Languages**.
- **Formato de Nome de Arquivo:** Personalize o formato do nome do arquivo de saída usando variáveis como `%(title)s`, `%(uploader)s`, `%(playlist_index)s` e `%(resolution)s`. Disponível em **Download Settings → Filename Format**.
- **Forçar Formato de Saída:** Força o download do vídeo em um formato de contêiner específico, como `mp4`, `webm` ou `mkv`. Disponível em **Download Settings → Output Format Settings**.
- **Conversão de Formato de Áudio:** Converta downloads de apenas áudio para formatos preferidos como `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, ou `Best`. Disponível em **Download Settings → Audio Format Settings**.
- **Normalização de Áudio:** Padroniza o volume para downloads de apenas áudio usando EBU R128.
- **Conexões Simultâneas:** Aumente significativamente a velocidade de download baixando arquivos em várias partes ao mesmo tempo. Disponível em **Download Settings → General → Concurrent Connections** (padrão 1, máximo 8-10 recomendado para evitar bloqueios de IP).
</details>
<details>
<summary>🌐 Acesso e Rede</summary>
- **Login por Cookies:** Faça login no YouTube usando cookies para acessar conteúdo privado.
Uso:
1. **Recomendado:** Use a opção integrada `Extract cookies from browser` no aplicativo, selecione o navegador e, opcionalmente, o perfil.
2. Opcionalmente, extraia cookies manualmente:
a. Exporte cookies do seu navegador usando uma extensão como [cookie-editor](https://github.com/moustachauve/cookie-editor?tab=readme-ov-file)
b. Copie os cookies no formato Netscape
c. Crie um arquivo chamado `cookies.txt` e cole os cookies
d. Selecione o arquivo `cookies.txt` no aplicativo
- **Suporte a Proxy:** Use um servidor proxy para downloads, ex: `http://<proxy-server>:<port>`
- **Modo Genérico:** Permite que o YTSage analise e baixe de sites além do YouTube suportados pelo yt-dlp. Habilite em **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Ferramentas e Manutenção</summary>
- **Comandos Personalizados:** Acesse recursos avançados do yt-dlp via argumentos de linha de comando.
- **Aba de Atualização:** Gerencie as ferramentas de atualização integradas em um só lugar nas Opções Personalizadas:
- **Atualização do yt-dlp:** Verifique atualizações e alterne entre os canais de lançamento Stable e Nightly.
- **Verificador de Versão do FFmpeg:** Verifique sua versão do FFmpeg e abra guias de instalação.
- **Atualização do Deno:** Verifique e atualize o runtime do Deno.
- **Detecção de FFmpeg/yt-dlp/Deno:** Detecta automaticamente caminhos e versões de FFmpeg, yt-dlp e Deno no diálogo Sobre.
- **Histórico de Downloads:** Veja downloads anteriores com miniaturas e status no botão **History**.
</details>
<details>
<summary>🌍 Localização</summary>
O YTSage suporta **14 idiomas** para alcance global. Escolha o seu idioma preferido em **Custom Options → Language**.
### Idiomas Suportados
| Idioma | Código | Idioma | Código |
|----------|------|----------|------|
| 🇺🇸 Inglês | `en` | 🇪🇸 Espanhol | `es` |
| 🇸🇦 Árabe | `ar` | 🇫🇷 Francês | `fr` |
| 🇩🇪 Alemão | `de` | 🇮🇳 Hindi | `hi` |
| 🇮🇩 Indonésio | `id` | 🇮🇹 Italiano | `it` |
| 🇯🇵 Japonês | `ja` | 🇵🇱 Polonês | `pl` |
| 🇧🇷 Português | `pt` | 🇷🇺 Russo | `ru` |
| 🇹🇷 Turco | `tr` | 🇨🇳 Chinês | `zh` |
### Traduções do README
| Idioma | Arquivo | Idioma | Arquivo |
|----------|------|----------|------|
| 🇺🇸 Inglês | [README.md](../README.md) | 🇪🇸 Espanhol | [README.es.md](README.es.md) |
| 🇸🇦 Árabe | [README.ar.md](README.ar.md) | 🇫🇷 Francês | [README.fr.md](README.fr.md) |
| 🇩🇪 Alemão | [README.de.md](README.de.md) | 🇮🇳 Hindi | [README.hi.md](README.hi.md) |
| 🇮🇩 Indonésio | [README.id.md](README.id.md) | 🇮🇹 Italiano | [README.it.md](README.it.md) |
| 🇯🇵 Japonês | [README.ja.md](README.ja.md) | 🇵🇱 Polonês | [README.pl.md](README.pl.md) |
| 🇧🇷 Português | [README.pt.md](README.pt.md) | 🇷🇺 Russo | [README.ru.md](README.ru.md) |
| 🇹🇷 Turco | [README.tr.md](README.tr.md) | 🇨🇳 Chinês | [README.zh.md](README.zh.md) |
> 💡 **Quer ajudar na tradução?** Veja a seção [Contribuindo](#contribuindo) para nos ajudar a adicionar mais idiomas!
</details>
<a id="solução-de-problemas"></a>
## 🛠️ Solução de Problemas
<details>
<summary>Clique para ver problemas comuns e soluções</summary>
- **Tabela de formatos não aparece:** Atualize o yt-dlp para a versão mais recente e tente alternar para o yt-dlp Nightly.
- **Download falhou:** Verifique sua conexão com a internet e certifique-se de que o vídeo está disponível.
- **Erros de Download Específicos:**
- **Vídeos Privados:** Use a autenticação por cookies para acessar conteúdo privado.
- **Conteúdo com Restrição de Idade:** Faça login na sua conta do YouTube para visualizar vídeos com restrição de idade.
- **Vídeos com Bloqueio Geográfico:** Considere usar uma VPN para contornar restrições regionais.
- **Vídeo Removido:** O vídeo não está mais disponível no YouTube.
- **Live Streams:** Transmissões ao vivo não podem ser baixadas enquanto estão sendo transmitidas; aguarde até que a transmissão termine.
- **Erros de Rede:** Verifique sua conexão com a internet e tente novamente.
- **URL Inválida:** Certifique-se de que a URL está correta e pertence a uma plataforma suportada.
- **Conteúdo Premium:** Requer uma assinatura do YouTube Premium.
- **Bloqueio por Direitos Autorais:** O conteúdo está bloqueado devido a restrições de direitos autorais.
- **Arquivos de vídeo e áudio estão separados após o download:** Isso acontece quando o FFmpeg está ausente ou não foi detectado. O YTSage requer o FFmpeg para mesclar streams de vídeo e áudio de alta qualidade.
- **Solução:** Certifique-se de que o FFmpeg está instalado e acessível no PATH do seu sistema. Para usuários do Windows, a opção mais fácil é baixar o arquivo `YTSage-v<versão>-ffmpeg.exe`, que já vem com o FFmpeg.
---
#### 🛡️ Aviso do Windows Defender / Antivírus
Alguns softwares antivírus podem sinalizar arquivos `.exe` como falsos positivos. Esta é uma **limitação conhecida** de aplicativos empacotados.
**Por que isso acontece:**
- As heurísticas do antivírus podem identificar incorretamente executáveis empacotados como suspeitos.
**Opções Seguras:**
- ✅ **Use a instalação via pip:** `pip install ytsage` (recomendado)
- ✅ **Compile a partir do código-fonte**: Seguindo este [guia](../.github/CI_CD_README.md)
- ✅ **Adicione o aplicativo à lista de permissões** do seu software antivírus.
#### 🍎 macOS: "App está danificado e não pode ser aberto"
Se você vir este erro no macOS Sonoma ou mais recente, você precisa remover o atributo de quarentena.
1. **Abra o Terminal** (você pode encontrá-lo usando o Spotlight).
2. **Digite o seguinte comando**, mas ainda **NÃO** pressione Enter. Certifique-se de incluir o espaço no final:
```bash
xattr -d com.apple.quarantine
```
3. **Arraste o arquivo `YTSage.app` da janela do Finder** e solte-o diretamente na janela do Terminal. Isso colará automaticamente o caminho correto do arquivo.
4. **Pressione Enter** para executar o comando.
5. **Tente abrir o YTSage.app novamente.** Ele agora deve iniciar corretamente.
---
#### **Localização da Configuração (Avançado)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="patrocinar"></a>
## 💖 Patrocinar
Se o YTSage economiza seu tempo, considere patrocinar o projeto. Os patrocínios ajudam a cobrir o tempo de desenvolvimento, testes em todas as plataformas e melhorias futuras.
- GitHub Sponsors: https://github.com/sponsors/oop7
- O link de patrocínio está disponível diretamente através do diálogo Sobre no aplicativo.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="contribuindo"></a>
## 👥 Contribuindo
Contribuições são bem-vindas! Veja como você pode ajudar:
1. 🍴 Faça um Fork do repositório
2. 🌿 Crie sua branch de recurso:
```bash
git checkout -b feature/RecursoIncrivel
```
3. 💾 Faça o commit de suas alterações:
```bash
git commit -m 'Adicionar RecursoIncrivel'
```
4. 📤 Faça o push para a branch:
```bash
git push origin feature/RecursoIncrivel
```
5. 🔄 Abra um Pull Request
### 🌍 Contribua com Traduções
- Atualize o arquivo README localizado relevante (ex: `readme-translations/README.pt.md`)
- Mantenha as strings do aplicativo em sincronia editando `ytsage/languages/<code>.json`
- Se o seu idioma estiver ausente, comece pelo `README.md` e crie `README.<code>.md`
<details>
<summary>📂 Estrutura do Projeto</summary>
## YTSage - Estrutura do Projeto
Este documento descreve a estrutura de pastas organizada do YTSage.
### 📁 Estrutura do Projeto
```
YTSage/
├── 📁 .github/ # Configurações do GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Modelos de problemas
│ │ └── 🐛-bug-report.md # Modelo de relatório de erro
│ ├─── 📁 workflows/ # Fluxos de trabalho do GitHub Actions
│ │ ├── build-linux.yml # Fluxo de build para Linux
│ │ ├── build-macos.yml # Fluxo de build para macOS
│ │ │── build-windows.yml # Fluxo de build para Windows
| | └── release-all.yml # Fluxo de lançamento principal
│ └── 📄 CI_CD_README.md # Documentação CI/CD
├── 📁 branding/ # Ativos de marca (capturas de tela, SVGs)
│ ├── 📁 icons/ # Ícones do aplicativo
│ ├── 📁 screenshots/ # Capturas de tela para documentação
│ └── 📁 svg/ # Ativos SVG
├── 📄 LICENSE # Arquivo de licença
├── 📄 pyproject.toml # Metadados do projeto e dependências
├── 📄 README.md # Documentação do projeto
├── 📄 requirements.txt # Dependências Python (dev)
└── 📁 ytsage/ # Pacote de código-fonte
├── 📁 assets/ # Ativos de tempo de execução
│ ├── 📁 Icon/ # Ícones do aplicativo
│ └── 📁 sound/ # Arquivos de áudio
├── 📁 languages/ # Arquivos de localização
│ ├── 📄 ar.json # Tradução em árabe
│ ├── 📄 de.json # Tradução em alemão
│ ├── 📄 en.json # Tradução em inglês
│ └── ... # Outros idiomas
├── 📁 core/ # Lógica de negócios principal
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Integração Deno
│ ├── 📄 ytsage_downloader.py # Funcionalidade de download
│ ├── 📄 ytsage_ffmpeg.py # Integração FFmpeg
│ ├── 📄 ytsage_utils.py # Funções utilitárias
│ └── 📄 ytsage_yt_dlp.py # Integração yt-dlp
├── 📁 gui/ # Componentes de interface do usuário
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # Janela principal do aplicativo
│ └── 📁 ytsage_gui_dialogs/ # Classes de diálogo
├── 📁 utils/ # Módulos utilitários
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # Gerenciamento de configuração
│ └── 📄 ytsage_logger.py # Ferramentas de registro
├── 📄 __init__.py # Ponto de entrada do pacote
└── 📄 main.py # Script de execução principal
```
</details>
## ⭐️ Histórico de Estrelas
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Licença
Este projeto é licenciado sob a Licença MIT - consulte o arquivo [LICENSE](../LICENSE) para mais detalhes.
## 🙏 Agradecimentos
<details>
<summary>Mostrar Agradecimentos</summary>
<div align="center">
<p>Muito obrigado a todos que contribuíram para este projeto abrindo problemas para sugerir melhorias ou relatar erros.</p>
<table>
<tr class="section"><th colspan="2">Componentes Principais</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Mecanismo de Download</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Processamento de Mídia</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Runtime para integração do yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Bibliotecas e Frameworks</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>Framework GUI</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Processamento de Imagens</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>Requisições HTTP</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Gerenciamento de Versão e Empacotamento</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Renderização de Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Registro de Logs</td>
</tr>
<tr class="section"><th colspan="2">Ativos e Contribuidores</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 de Universfield</a></td>
<td>Som de Notificação</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Contribuidor de Código</td>
</tr>
</table>
</div>
</details>
## ⚠️ Isenção de Responsabilidade
Esta ferramenta é apenas para uso pessoal. Respeite os Termos de Serviço do YouTube e os direitos dos produtores de conteúdo.
---
<div align="center">
Criado com ❤️ por [oop7](https://github.com/oop7)
</div>
+613
View File
@@ -0,0 +1,613 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="Интерфейс YTSage"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Современный загрузчик с YouTube с чистым интерфейсом на PySide6.**
Загружайте видео в любом качестве, извлекайте аудио, получайте субтитры и многое другое.
### 🌍 Языки README
Английский: [EN](../README.md)
| Арабский: [AR](README.ar.md)
| Немецкий: [DE](README.de.md)
| Испанский: [ES](README.es.md)
| Французский: [FR](README.fr.md)
| Хинди: [HI](README.hi.md)
| Индонезийский: [ID](README.id.md)
| Итальянский: [IT](README.it.md)
| Японский: [JA](README.ja.md)
| Польский: [PL](README.pl.md)
| Португальский: [PT](README.pt.md)
| Русский: [RU](README.ru.md)
| Турецкий: [TR](README.tr.md)
| Китайский: [ZH](README.zh.md)
<p align="center">
<a href="#установка">Установка</a> •
<a href="#функции">Функции</a> •
<a href="#использование">Использование</a> •
<a href="#скриншоты">Скриншоты</a> •
<a href="#решение-проблем">Решение проблем</a> •
<a href="#спонсорство">Спонсорство</a> •
<a href="#участие-в-проекте">Участие в проекте</a>
</p>
</div>
---
<a id="почему-ytsage"></a>
## ❓ Почему YTSage?
YTSage создан для пользователей, которым нужен **простой, но мощный загрузчик с YouTube**. В отличие от других инструментов, он предлагает:
- Современный и чистый интерфейс PySide6
- Загрузку видео, аудио и субтитров одним щелчком мыши
- Дополнительные функции, такие как SponsorBlock, объединение субтитров и выбор плейлиста
- Опциональный "Общий режим" (Generic Mode) для сайтов помимо YouTube, поддерживаемых yt-dlp
- Кроссплатформенную поддержку и простую установку
<a id="функции"></a>
## ✨ Функции
<div align="center">
| Основные функции | Продвинутые функции | Дополнительные возможности |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Таблица форматов | 🚫 Интеграция SponsorBlock | 🎞️ Отображение FPS/HDR |
| 🎵 Извлечение аудио | 📝 Выбор и объединение субтитров | 🔄 Автообновление yt-dlp |
| ✨ Простой пользовательский интерфейс | 💾 Сохранение описания и обложки | 🛠️ Обнаружение FFmpeg/yt-dlp/Deno |
| 📋 Поддержка и выбор плейлиста | 🚀 Ограничение скорости | ⚙️ Пользовательские команды |
| 📑 Интеграция разделов | ✂️ Обрезка видео | 🍪 Вход через Cookies |
| 📜 История загрузок | 🔄 Выбор канала выпуска | 🌐 Поддержка прокси |
| 🎚️ Конвертация аудиоформатов | 🎬 Настройки видеоформата | 🆙 Встроенная вкладка обновления |
| 🌍 Общий режим | 🔊 Нормализация звука (EBU R128) | 🌍 Локализация на 14 языков |
| 💾 Экспорт плейлиста | ⚙️ Качество и субтитры по умолчанию | |
</div>
<a id="установка"></a>
## 🚀 Установка
### ⚡ Быстрая установка (Рекомендуется)
Установите YTSage через PyPI:
```bash
pip install ytsage
```
<details>
<summary>🔄 Обновить существующую установку</summary>
```bash
pip install --upgrade ytsage
```
</details>
Затем запустите приложение:
```bash
ytsage
```
### 📦 Готовые исполняемые файлы (Executable)
> [👉 Скачать последний релиз](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Формат | Описание |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Стандартный установщик |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | С включенным FFmpeg |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Портативная версия (не требует установки) |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Портативная с FFmpeg, сжатая (ZIP) |
<details>
<summary>🛠️ Шаги по установке</summary>
1. **EXE установщик (`.exe`)**: Дважды щелкните файл и следуйте инструкциям мастера установки.
2. **Портативная версия (`.zip`)**: Распакуйте файл в нужное место и запустите `ytsage.exe`.
3. **Встроенный FFmpeg**: Если в вашей системе не установлен FFmpeg, выбирайте версии со встроенным FFmpeg.
</details>
#### 🐧 Linux
| Формат | Описание |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Пакет Debian |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, Портативный |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Пакет RPM |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Пакет Flatpak |
<details>
<summary>🛠️ Шаги по установке</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Если нужно исправить зависимости
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Следуйте инструкциям на Flathub или выполните:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Формат | Описание |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Приложение ZIP для Apple Silicon |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Установщик DMG для Apple Silicon |
<details>
<summary>🛠️ Шаги по установке</summary>
- **DMG установщик (`.dmg`)**: Дважды щелкните, чтобы смонтировать, и перетащите `YTSage.app` в папку Applications (Программы).
- **Приложение в архиве (`.zip`)**: Распакуйте ZIP и переместите `YTSage.app` в папку Applications.
*Примечание: Если вы получили ошибку "App is damaged", см. раздел по macOS ниже.*
</details>
---
<details>
<summary>💻 Ручная установка из исходного кода</summary>
### 1. Клонировать репозиторий
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Установить зависимости
#### ⚡ С помощью uv
```bash
uv pip install .
```
#### 📦 Или через стандартный pip
```bash
pip install .
```
### 3. Запустить приложение
```bash
python -m ytsage.main
```
</details>
<a id="скриншоты"></a>
## 📸 Скриншоты
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="Настройки загрузки" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Загрузка плейлиста" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Настройки загрузки</em></td>
<td align="center"><em>Загрузка плейлиста</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Выбор аудиоформата" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Пользовательские опции" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Аудиоформат</em></td>
<td align="center"><em>Пользовательские опции</em></td>
</tr>
</table>
</div>
<a id="использование"></a>
## 📖 Использование
<details>
<summary>🎯 Базовое использование</summary>
1. **Запустите YTSage**
2. **Вставьте ссылку на YouTube** (или используйте кнопку "Paste URL")
3. **Нажмите "Analyze"**
4. **Выберите формат:**
- `Video` для загрузки видео
- `Audio Only` для извлечения аудио
5. **Выберите опции:**
- Включите субтитры и выберите язык
- Включите объединение субтитров (Merge)
- Сохранить обложку (Save Thumbnail)
- Удалить спонсорские вставки (SponsorBlock)
- Сохранить описание (Save Description)
- Внедрить главы (Embed Chapters)
6. **Выберите папку для сохранения**
7. **Нажмите "Download"**
> 💡 Папка загрузок по умолчанию — папка "Загрузки" текущего пользователя.
</details>
<details>
<summary>📋 Загрузка плейлиста</summary>
1. **Вставьте ссылку на плейлист**
2. **Нажмите "Analyze"**
3. **Выберите видео в селекторе (по умолчанию выбраны все)**
4. **Выберите нужный формат/качество**
5. **Нажмите "Download"**
> 💡 Приложение автоматически управляет очередью загрузки, и вы можете экспортировать записи плейлиста в файлы `.txt`, `.csv`, `.m3u` или `.json`.
</details>
<details>
<summary>🌍 Общий режим для сайтов помимо YouTube</summary>
Используйте Общий режим (Generic Mode), когда хотите, чтобы YTSage принимал ссылки с других сайтов, поддерживаемых yt-dlp, таких как Dailymotion, TikTok и другие.
Как использовать:
1. Откройте `Download Settings`.
2. Включите `Generic Mode`.
3. Вставьте ссылку на видео или плейлист с поддерживаемого сайта (не YouTube).
4. Нажмите `Analyze`.
5. Выберите формат и скачайте как обычно.
Примечания:
- Общий режим только отключает строгую проверку URL внутри YTSage. Сайт все равно должен поддерживаться установленной версией yt-dlp.
- Некоторым сайтам требуются cookies, вход, прокси или дополнительные аргументы yt-dlp.
- Если сайт перестал работать, обновите yt-dlp во встроенной вкладке обновления, прежде чем сообщать о проблеме.
</details>
<details>
<summary>🧰 Опции медиа и загрузки</summary>
- **Опции субтитров:** Фильтруйте языки и внедряйте субтитры в видеофайл.
- **Объединение субтитров:** "Вшивает" субтитры в видеофайл (hardcode).
- **Сохранить описание:** Сохраняет описание видео в текстовый файл.
- **Сохранить обложку:** Сохраняет превью видео как изображение.
- **Внедрить главы:** Добавляет маркеры глав в метаданные для совместимых медиаплееров.
- **Удалить спонсорские вставки:** Использует SponsorBlock для удаления рекламных сегментов из видео.
- **Обрезать видео:** Загружайте только части видео, указав временной интервал в формате `HH:MM:SS`.
</details>
<details>
<summary>⚙️ Настройки вывода и файлов</summary>
- **Ограничение скорости:** Ограничьте скорость загрузки, например, `500K` для 500 КБ/с.
- **Путь загрузки:** Сохраните путь по умолчанию для будущих загрузок. Доступно в **Download Settings → Download Path**.
- **Разрешение по умолчанию:** Установите предпочитаемое разрешение для автовыбора (например, 1080p, 720p). Доступно в **Download Settings → Default Video Resolution**.
- **Языки субтитров по умолчанию:** Установите языки по умолчанию для автовыбора (через запятую, например, `ru,en`). Доступно в **Download Settings → Default Subtitle Languages**.
- **Формат имени файла:** Настройте формат имени выходного файла, используя переменные типа `%(title)s`, `%(uploader)s` и др. Доступно в **Download Settings → Filename Format**.
- **Принудительный формат вывода:** Принудительно скачивайте видео в определенном контейнере, например `mp4`, `webm` или `mkv`. Доступно в **Download Settings → Output Format Settings**.
- **Конвертация аудио:** Конвертируйте аудио в форматы `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis`, или `Best`. Доступно в **Download Settings → Audio Format Settings**.
- **Нормализация звука:** Выравнивает громкость аудио загрузок согласно EBU R128.
- **Одновременные соединения:** Значительно увеличивает скорость за счет загрузки в несколько потоков. Доступно в **Download Settings → General → Concurrent Connections** (рекомендуется 8-10).
</details>
<details>
<summary>🌐 Доступ и сеть</summary>
- **Вход через Cookies:** Войдите в YouTube через куки для доступа к приватному контенту.
Как использовать:
1. **Рекомендуется:** Используйте встроенную опцию `Extract cookies from browser`, выберите браузер и профиль.
2. Или вручную:
a. Экспортируйте куки через расширение [cookie-editor](https://github.com/moustachauve/cookie-editor).
b. Скопируйте куки в формате Netscape.
c. Создайте файл `cookies.txt` и вставьте их туда.
d. Выберите этот файл в приложении.
- **Поддержка прокси:** Используйте прокси-сервер для загрузок, например: `http://<proxy-server>:<port>`
- **Общий режим:** Позволяет скачивать с сайтов помимо YouTube. Включите в **Download Settings → Generic Mode**.
</details>
<details>
<summary>🛠️ Инструменты и обслуживание</summary>
- **Пользовательские команды:** Доступ к продвинутым функциям yt-dlp через аргументы командной строки.
- **Вкладка обновления:** Управляйте инструментами в одном месте (Custom Options):
- **Обновление yt-dlp:** Проверка обновлений, переключение между Stable и Nightly ветками.
- **Версия FFmpeg:** Проверка версии и инструкции по установке.
- **Обновление Deno:** Проверка и обновление Deno.
- **Обнаружение FFmpeg/yt-dlp/Deno:** Автоматически находит пути и версии в окне "About".
- **История загрузок:** Просматривайте историю с обложками и статусом через кнопку **History**.
</details>
<details>
<summary>🌍 Локализация</summary>
YTSage поддерживает **14 языков**. Выберите нужный в **Custom Options → Language**.
### Поддерживаемые языки
| Язык | Код | Язык | Код |
|----------|------|----------|------|
| 🇺🇸 Английский | `en` | 🇪🇸 Испанский | `es` |
| 🇸🇦 Арабский | `ar` | 🇫🇷 Французский | `fr` |
| 🇩🇪 Немецкий | `de` | 🇮🇳 Хинди | `hi` |
| 🇮🇩 Индонезийский | `id` | 🇮🇹 Итальянский | `it` |
| 🇯🇵 Японский | `ja` | 🇵🇱 Польский | `pl` |
| 🇧🇷 Португальский | `pt` | 🇷🇺 Русский | `ru` |
| 🇹🇷 Турецкий | `tr` | 🇨🇳 Китайский | `zh` |
### Переводы README
| Язык | Файл | Язык | Файл |
|----------|------|----------|------|
| 🇺🇸 Английский | [README.md](../README.md) | 🇪🇸 Испанский | [README.es.md](README.es.md) |
| 🇸🇦 Арабский | [README.ar.md](README.ar.md) | 🇫🇷 Французский | [README.fr.md](README.fr.md) |
| 🇩🇪 Немецкий | [README.de.md](README.de.md) | 🇮🇳 Хинди | [README.hi.md](README.hi.md) |
| 🇮🇩 Индонезийский | [README.id.md](README.id.md) | 🇮🇹 Итальянский | [README.it.md](README.it.md) |
| 🇯🇵 Японский | [README.ja.md](README.ja.md) | 🇵🇱 Польский | [README.pl.md](README.pl.md) |
| 🇧🇷 Португальский | [README.pt.md](README.pt.md) | 🇷🇺 Русский | [README.ru.md](README.ru.md) |
| 🇹🇷 Турецкий | [README.tr.md](README.tr.md) | 🇨🇳 Китайский | [README.zh.md](README.zh.md) |
> 💡 **Хотите помочь с переводом?** Ознакомьтесь с разделом [Участие в проекте](#участие-в-проекте), чтобы помочь нам добавить новые языки!
</details>
<a id="решение-проблем"></a>
## 🛠️ Решение проблем
<details>
<summary>Нажмите, чтобы увидеть частые вопросы</summary>
- **Не появляется таблица форматов:** Обновите yt-dlp до последней версии и попробуйте переключиться на канал Nightly.
- **Загрузка не удалась:** Проверьте интернет-соединение и доступность видео.
- **Специфические ошибки:**
- **Приватные видео:** Используйте вход через Cookies.
- **Ограничение по возрасту:** Войдите в аккаунт YouTube.
- **Геоблокировка:** Используйте VPN.
- **Видео удалено:** Видео больше недоступно на YouTube.
- **Прямые трансляции:** Стримы нельзя скачать, пока они идут; дождитесь завершения.
- **Сетевые ошибки:** Проверьте подключение к интернету и повторите попытку.
- **Неверный URL:** Убедитесь, что URL указан правильно и принадлежит поддерживаемой платформе.
- **Премиум-контент:** Требуется подписка YouTube Premium.
- **Блокировка авторских прав:** Контент заблокирован из-за ограничений авторского права.
- **Видео и аудио разделены после загрузки:** Это происходит, если FFmpeg отсутствует или не обнаружен. YTSage нужен FFmpeg для объединения потоков.
- **Решение:** Убедитесь, что FFmpeg установлен и добавлен в PATH. Пользователям Windows проще всего скачать версию с `-ffmpeg.exe`.
---
#### 🛡️ Предупреждение Windows Defender / Антивируса
Некоторые антивирусы могут помечать `.exe` файлы как ложные срабатывания. Это **известная особенность** упакованных приложений.
**Причины:**
- Эвристические алгоритмы антивирусов могут ошибочно идентифицировать упакованные исполняемые файлы как подозрительные.
**Безопасные варианты:**
- ✅ **Установка через pip:** `pip install ytsage` (рекомендуется)
- ✅ **Сборка из исходников**: Инструкция [здесь](../.github/CI_CD_README.md)
- ✅ **Добавить в исключения** антивируса.
#### 🍎 macOS: "App is damaged and cannot be opened"
Если вы видите эту ошибку на macOS Sonoma или новее:
1. **Откройте Терминал**.
2. **Введите команду** (с пробелом в конце):
```bash
xattr -d com.apple.quarantine
```
3. **Перетащите `YTSage.app`** из Finder в окно Терминала.
4. **Нажмите Enter**.
5. Попробуйте открыть программу снова.
---
#### **Пути конфигурации (Продвинутым)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="спонсорство"></a>
## 💖 Спонсорство
Если YTSage экономит ваше время, поддержите разработку проекта. Спонсорство помогает поддерживать тестирование на всех платформах и внедрять новые функции.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Ссылка на спонсорство также есть в окне "About" в приложении.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="участие-в-проекте"></a>
## 👥 Участие в проекте
Мы рады любой помощи!
1. 🍴 Сделайте Fork репозитория
2. 🌿 Создайте ветку: `git checkout -b feature/NewFeature`
3. 💾 Сделайте коммит: `git commit -m 'Add NewFeature'`
4. 📤 Отправите изменения: `git push origin feature/NewFeature`
5. 🔄 Создайте Pull Request
### 🌍 Помощь с переводами
- Обновите файл README (например, `readme-translations/README.ru.md`)
- Обновите строки интерфейса в `ytsage/languages/<code>.json`
- Если ваш язык отсутствует, начните с `README.md` и создайте `README.<code>.md`
<details>
<summary>📂 Структура проекта</summary>
## YTSage - Структура проекта
Этот документ описывает организованную структуру папок YTSage.
### 📁 Схема проекта
```
YTSage/
├── 📁 .github/ # Настройки GitHub
│ ├── 📁 ISSUE_TEMPLATE/ # Шаблоны проблем
│ │ └── 🐛-bug-report.md # Шаблон отчета об ошибке
│ ├─── 📁 workflows/ # Рабочие процессы GitHub Actions
│ │ ├── build-linux.yml # Сборка для Linux
│ │ ├── build-macos.yml # Сборка для macOS
│ │ │── build-windows.yml # Сборка для Windows
| | └── release-all.yml # Основной процесс выпуска
│ └── 📄 CI_CD_README.md # Документация CI/CD
├── 📁 branding/ # Брендинг (скриншоты, SVG)
│ ├── 📁 icons/ # Иконки приложения
│ ├── 📁 screenshots/ # Скриншоты для документации
│ └── 📁 svg/ # SVG ассеты
├── 📄 LICENSE # Файл лицензии
├── 📄 pyproject.toml # Метаданные проекта и зависимости
├── 📄 README.md # Документация проекта
├── 📄 requirements.txt # Зависимости Python (разработка)
└── 📁 ytsage/ # Исходный код
├── 📁 assets/ # Ресурсы времени выполнения
│ ├── 📁 Icon/ # Иконки приложения
│ └── 📁 sound/ # Звуковые файлы
├── 📁 languages/ # Файлы локализации
│ ├── 📄 ar.json # Перевод на арабский
│ ├── 📄 de.json # Перевод на немецкий
│ ├── 📄 en.json # Перевод на английский
│ └── ... # Другие языки
├── 📁 core/ # Основная бизнес-логика
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Интеграция Deno
│ ├── 📄 ytsage_downloader.py # Функционал загрузки
│ ├── 📄 ytsage_ffmpeg.py # Интеграция FFmpeg
│ ├── 📄 ytsage_utils.py # Вспомогательные функции
│ └── 📄 ytsage_yt_dlp.py # Интеграция yt-dlp
├── 📁 gui/ # Компоненты интерфейса
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # Главное окно приложения
│ └── 📁 ytsage_gui_dialogs/ # Классы диалогов
├── 📁 utils/ # Утилиты
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # Управление конфигурацией
│ └── 📄 ytsage_logger.py # Логирование
├── 📄 __init__.py # Точка входа в пакет
└── 📄 main.py # Основной скрипт запуска
```
</details>
## ⭐️ История звезд
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Лицензия
Проект распространяется под лицензией MIT — подробности в файле [LICENSE](../LICENSE).
## 🙏 Благодарности
<details>
<summary>Показать благодарности</summary>
<div align="center">
<p>Большое спасибо всем, кто внес свой вклад в этот проект, открывая проблемы с предложениями по улучшению или отчетами об ошибках.</p>
<table>
<tr class="section"><th colspan="2">Основные компоненты</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>Движок загрузки</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Обработка медиа</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>Среда для интеграции yt-dlp</td>
</tr>
<tr class="section"><th colspan="2">Библиотеки и фреймворки</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI фреймворк</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Обработка изображений</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>HTTP запросы</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Управление версиями</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Рендеринг Markdown</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Логирование</td>
</tr>
<tr class="section"><th colspan="2">Контент и участники</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">New Notification 09 от Universfield</a></td>
<td>Звук уведомления</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Участник разработки</td>
</tr>
</table>
</div>
</details>
## ⚠️ Отказ от ответственности
Этот инструмент предназначен только для личного использования. Пожалуйста, соблюдайте Условия использования YouTube и права создателей контента.
---
<div align="center">
Сделано с ❤️ от [oop7](https://github.com/oop7)
</div>
+624
View File
@@ -0,0 +1,624 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage Arayüzü"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI Downloads](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub Downloads](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![Supported Platforms](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI version](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**Temiz bir PySide6 arayüzüne sahip modern bir YouTube indiricisi.**
Videoları herhangi bir kalitede indirin, sesleri çıkarın, altyazıları alın ve daha fazlasını yapın.
### 🌍 README Dilleri
İngilizce: [EN](../README.md)
| Arapça: [AR](README.ar.md)
| Almanca: [DE](README.de.md)
| İspanyolca: [ES](README.es.md)
| Fransızca: [FR](README.fr.md)
| Hintçe: [HI](README.hi.md)
| Endonezce: [ID](README.id.md)
| İtalyanca: [IT](README.it.md)
| Japonca: [JA](README.ja.md)
| Lehçe: [PL](README.pl.md)
| Portekizce: [PT](README.pt.md)
| Rusça: [RU](README.ru.md)
| Türkçe: [TR](README.tr.md)
| Çince: [ZH](README.zh.md)
<p align="center">
<a href="#kurulum">Kurulum</a> •
<a href="#özellikler">Özellikler</a> •
<a href="#kullanım">Kullanım</a> •
<a href="#ekran-görüntüleri">Ekran Görüntüleri</a> •
<a href="#sorun-giderme">Sorun Giderme</a> •
<a href="#sponsor-olun">Sponsor Olun</a> •
<a href="#katkıda-bulunma">Katkıda Bulunma</a>
</p>
</div>
---
<a id="neden-ytsage"></a>
## ❓ Neden YTSage?
YTSage, **basit ama güçlü bir YouTube indiricisi** isteyen kullanıcılar için tasarlanmıştır. Diğer araçların aksine şunları sunar:
- Modern ve temiz bir PySide6 arayüzü
- Tek tıkla video, ses ve altyazı indirme
- SponsorBlock, altyazı birleştirme ve oynatma listesi seçimi gibi gelişmiş özellikler
- yt-dlp tarafından desteklenen YouTube dışındaki siteler için isteğe bağlı "Genel Mod"
- Çoklu platform desteği ve kolay kurulum
<a id="özellikler"></a>
## ✨ Özellikler
<div align="center">
| Temel Özellikler | Gelişmiş Özellikler | Ekstra Özellikler |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 Format Tablosu | 🚫 SponsorBlock Entegrasyonu | 🎞️ FPS/HDR Gösterimi |
| 🎵 Ses Çıkarma | 📝 Altyazı Seçimi ve Birleştirme | 🔄 Otomatik yt-dlp Güncellemesi |
| ✨ Basit Kullanıcı Arayüzü | 💾 Açıklama ve Küçük Resim Kaydetme | 🛠️ FFmpeg/yt-dlp/Deno Algılama |
| 📋 Oynatma Listesi Desteği | 🚀 Hız Sınırlayıcı | ⚙️ Özel Komutlar |
| 📑 Bölüm Entegrasyonu | ✂️ Video Kırpma | 🍪 Çerez ile Giriş |
| 📜 İndirme Geçmişi | 🔄 Yayın Kanalı Seçimi | 🌐 Proxy Desteği |
| 🎚️ Ses Formatı Dönüştürme | 🎬 Video Format Ayarları | 🆙 Entegre Güncelleme Sekmesi |
| 🌍 Genel Mod | 🔊 Ses Normalizasyonu (EBU R128) | 🌍 14 Dilde Yerelleştirme |
| 💾 Oynatma Listesi Dışa Aktarma | ⚙️ Varsayılan Kalite ve Altyazı | |
</div>
<a id="kurulum"></a>
## 🚀 Kurulum
### ⚡ Hızlı Kurulum (Önerilen)
YTSage'i PyPI üzerinden yükleyin:
```bash
pip install ytsage
```
<details>
<summary>🔄 Mevcut Kurulumu Güncelle</summary>
```bash
pip install --upgrade ytsage
```
</details>
Ardından uygulamayı çalıştırın:
```bash
ytsage
```
### 📦 Hazır Çalıştırılabilir Dosyalar (Executable)
> [👉 En Son Sürümü İndir](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| Format | Açıklama |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Standart Kurulum Dosyası |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | FFmpeg Dahil |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | Taşınabilir Sürüm, kuruluma gerek yok |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | FFmpeg Dahil Taşınabilir, sıkıştırılmış (ZIP) |
<details>
<summary>🛠️ Kurulum Adımları</summary>
1. **EXE Yükleyici (`.exe`)**: Dosyaya çift tıklayın ve kurulum sihirbazını takip edin.
2. **Taşınabilir Sürüm (`.zip`)**: Dosyayı istediğiniz yere çıkarın ve `ytsage.exe` dosyasını çalıştırın.
3. **Dahili FFmpeg**: Sisteminizde FFmpeg kurulu değilse, FFmpeg dahil olan sürümleri seçin.
</details>
#### 🐧 Linux
| Format | Açıklama |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian Paketi |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | AppImage, Taşınabilir |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM Paketi |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak Paketi |
<details>
<summary>🛠️ Kurulum Adımları</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # Gerekirse eksik bağımlılıkları gidermek için
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: Flathub üzerindeki talimatları izleyin veya şunu çalıştırın:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| Format | Açıklama |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon için ZIP Uygulaması |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon için Disk Image Kurulum Dosyası |
<details>
<summary>🛠️ Kurulum Adımları</summary>
- **DMG Yükleyici (`.dmg`)**: Bağlamak için çift tıklayın ve `YTSage.app` dosyasını Uygulamalar klasörünüze sürükleyin.
- **Uygulama Arşivi (`.zip`)**: ZIP dosyasını çıkarın ve `YTSage.app` dosyasını Uygulamalar klasörünüze taşıyın.
*Not: "Uygulama hasarlı" hatası alırsanız, aşağıdaki macOS Sorun Giderme bölümüne bakın.*
</details>
---
<details>
<summary>💻 Kaynak Kodundan Manuel Kurulum</summary>
### 1. Depoyu Klonlayın
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. Bağımlılıkları Yükleyin
#### ⚡ uv ile
```bash
uv pip install .
```
#### 📦 Veya standart pip ile
```bash
pip install .
```
### 3. Uygulamayı Çalıştırın
```bash
python -m ytsage.main
```
</details>
<a id="ekran-görüntüleri"></a>
## 📸 Ekran Görüntüleri
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="İndirme Ayarları" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="Oynatma Listesi İndirme" width="400"/></td>
</tr>
<tr>
<td align="center"><em>İndirme Ayarları</em></td>
<td align="center"><em>Oynatma Listesi İndirme</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="Ses Formatı Seçimi" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="Özel Seçenekler" width="400"/></td>
</tr>
<tr>
<td align="center"><em>Ses Formatı</em></td>
<td align="center"><em>Özel Seçenekler</em></td>
</tr>
</table>
</div>
<a id="kullanım"></a>
## 📖 Kullanım
<details>
<summary>🎯 Temel Kullanım</summary>
1. **YTSage'i başlatın**
2. **Bir YouTube URL'si yapıştırın** (veya "Paste URL" düğmesini kullanın)
3. **"Analyze" düğmesine tıklayın**
4. **Formatı Seçin:**
- Video indirmek için `Video`
- Sadece ses çıkarmak için `Audio Only`
5. **Seçenekleri Belirleyin:**
- Altyazıları etkinleştirin ve dili seçin
- Altyazı birleştirmeyi (Merge subs) etkinleştirin
- Küçük resmi kaydet (Save thumbnail)
- Sponsor bölümlerini kaldır (SponsorBlock)
- Açıklamayı kaydet (Save description)
- Bölümleri göm (Embed chapters)
6. **Çıkış Dizinini Seçin**
7. **"Download" düğmesine tıklayın**
> 💡 Varsayılan indirme dizini kullanıcının "İndirmeler" klasörüdür.
</details>
<details>
<summary>📋 Oynatma Listesi İndirme</summary>
1. **Oynatma Listesi URL'sini yapıştırın**
2. **"Analyze" düğmesine tıklayın**
3. **Seçiciden videoları seçin (varsayılan olarak tümü seçilidir)**
4. **İstediğiniz formatı/kaliteyi seçin**
5. **"Download" düğmesine tıklayın**
> 💡 Uygulama indirme kuyruğunu otomatik olarak yönetir ve oynatma listesi girişlerini `.txt`, `.csv`, `.m3u` veya `.json` dosyaları olarak dışa aktarabilirsiniz.
</details>
<details>
<summary>🌍 YouTube Dışındaki Siteler İçin Genel Mod</summary>
YTSage'in Dailymotion, TikTok ve diğerleri gibi yt-dlp tarafından desteklenen sitelerden gelen URL'leri kabul etmesini istediğinizde Genel Modu kullanın.
Nasıl kullanılır:
1. `Download Settings` bölümünü açın.
2. `Generic Mode` seçeneğini etkinleştirin.
3. YouTube dışındaki desteklenen bir video veya oynatma listesi URL'sini yapıştırın.
4. `Analyze` düğmesine tıklayın.
5. Bir format seçin ve normal şekilde indirin.
Notlar:
- Genel Mod sadece YTSage içindeki URL doğrulamasını değiştirir. Hedef site hala yüklü yt-dlp sürümünüz tarafından desteklenmelidir.
- Bazı siteler, çıkarıcıya bağlı olarak çerezler, giriş, proxy veya ek yt-dlp argümanları gerektirebilir.
- Bir site hata verirse, sorun bildirmeden önce entegre güncelleme sekmesinden yt-dlp'yi güncelleyin.
</details>
<details>
<summary>🧰 Medya ve İndirme Seçenekleri</summary>
- **Altyazı Seçenekleri:** Dilleri filtreleyin ve altyazıları video dosyasına gömün.
- **Altyazı Birleştirme:** Altyazıları video dosyasına kalıcı olarak (hardcode) birleştirir.
- **Açıklamayı Kaydet:** Video açıklamasını bir metin dosyası olarak kaydeder.
- **Küçük Resmi Kaydet:** Video küçük resmini bir resim dosyası olarak kaydeder.
- **Bölümleri Göm:** Uyumlu video oynatıcılar için meta veri olarak bölüm işaretlerini ekler.
- **Sponsor Bölümlerini Kaldır:** Videodaki sponsorlu bölümleri kaldırmak için SponsorBlock kullanır.
- **Videoyu Kırp:** Zaman aralığını `SA:DA:SA` formatında belirterek videonun sadece belirli bölümlerini indirin.
</details>
<details>
<summary>⚙️ Çıkış ve Dosya Ayarları</summary>
- **Hız Sınırlayıcı:** İndirme hızını sınırlandırın, örneğin 500 KB/s için `500K`.
- **İndirme Yolunu Kaydet:** Gelecekteki indirmeler için varsayılan indirme yolunu kaydeder. **Download Settings → Download Path** bölümünde mevcuttur.
- **Varsayılan Video Çözünürlüğü:** Otomatik seçim için tercih ettiğiniz video çözünürlüğünü ayarlayın (örn: 1080p, 720p). **Download Settings → Default Video Resolution** bölümünde mevcuttur.
- **Varsayılan Altyazı Dilleri:** Otomatik seçim için varsayılan altyazı dillerini ayarlayın (virgülle ayrılmış, örn: `tr,en`). **Download Settings → Default Subtitle Languages** bölümünde mevcuttur.
- **Dosya Adı Formatı:** Çıkış dosyası adı formatını `%(title)s`, `%(uploader)s` gibi değişkenler kullanarak özelleştirin. **Download Settings → Filename Format** bölümünde mevcuttur.
- **Çıkış Formatını Zorla:** Videoyu `mp4`, `webm` veya `mkv` gibi belirli bir konteyner formatında indirmeye zorlar. **Download Settings → Output Format Settings** bölümünde mevcuttur.
- **Ses Formatı Dönüştürme:** Sadece ses indirmelerini `AAC`, `MP3`, `FLAC`, `WAV`, `Opus`, `M4A`, `Vorbis` veya `Best` gibi tercih edilen formatlara dönüştürün. **Download Settings → Audio Format Settings** bölümünde mevcuttur.
- **Ses Normalizasyonu:** EBU R128 kullanarak sadece ses indirmeleri için ses seviyesini standartlaştırır.
- **Eşzamanlı Bağlantılar:** Dosyaları aynı anda birden fazla parça halinde indirerek indirme hızını önemli ölçüde artırın. **Download Settings → General → Concurrent Connections** bölümünde mevcuttur (Varsayılan 1, IP engellemelerini önlemek için maksimum 8-10 önerilir).
</details>
<details>
<summary>🌐 Erişim ve Ağ</summary>
- **Çerez ile Giriş:** Özel içeriğe erişmek için çerezleri kullanarak YouTube'da oturum açın.
Kullanım:
1. **Önerilen:** Uygulamadaki entegre `Extract cookies from browser` seçeneğini kullanın, tarayıcıyı ve isteğe bağlı olarak profili seçin.
2. İsteğe bağlı olarak çerezleri manuel olarak çıkarın:
a. [cookie-editor](https://github.com/moustachauve/cookie-editor) gibi bir uzantı kullanarak tarayıcınızdan çerezleri dışa aktarın.
b. Çerezleri Netscape formatında kopyalayın.
c. `cookies.txt` adlı bir dosya oluşturun ve çerezleri yapıştırın.
d. Uygulamada `cookies.txt` dosyasını seçin.
- **Proxy Desteği:** İndirmeler için bir proxy sunucusu kullanın, örn: `http://<proxy-server>:<port>`
- **Genel Mod:** YTSage'in yt-dlp tarafından desteklenen YouTube dışındaki siteleri analiz etmesine ve indirmesine olanak tanır. **Download Settings → Generic Mode** bölümünden etkinleştirin.
</details>
<details>
<summary>🛠️ Araçlar ve Bakım</summary>
- **Özel Komutlar:** Komut satırı argümanları aracılığıyla gelişmiş yt-dlp özelliklerine erişin.
- **Güncelleme Sekmesi:** Entegre güncelleme araçlarını Özel Seçenekler altındaki tek bir yerden yönetin:
- **yt-dlp Güncelleme:** Güncellemeleri kontrol edin ve Stable ile Nightly yayın kanalları arasında geçiş yapın.
- **FFmpeg Sürüm Kontrolü:** FFmpeg sürümünüzü kontrol edin ve kurulum kılavuzlarını açın.
- **Deno Güncelleme:** Deno çalışma zamanını kontrol edin ve güncelleyin.
- **FFmpeg/yt-dlp/Deno Algılama:** Hakkında diyaloğunda FFmpeg, yt-dlp ve Deno yollarını ve sürümlerini otomatik olarak algılar.
- **İndirme Geçmişi:** **History** düğmesi aracılığıyla küçük resimler ve durumlarla birlikte geçmiş indirmeleri görün.
</details>
<details>
<summary>🌍 Yerelleştirme</summary>
YTSage, küresel erişim için **14 dili** destekler. Tercih ettiğiniz dili **Custom Options → Language** bölümünden seçin.
### Desteklenen Diller
| Dil | Kod | Dil | Kod |
|----------|------|----------|------|
| 🇺🇸 İngilizce | `en` | 🇪🇸 İspanyolca | `es` |
| 🇸🇦 Arapça | `ar` | 🇫🇷 Fransızca | `fr` |
| 🇩🇪 Almanca | `de` | 🇮🇳 Hintçe | `hi` |
| 🇮🇩 Endonezce | `id` | 🇮🇹 İtalyanca | `it` |
| 🇯🇵 Japonca | `ja` | 🇵🇱 Lehçe | `pl` |
| 🇧🇷 Portekizce | `pt` | 🇷🇺 Rusça | `ru` |
| 🇹🇷 Türkçe | `tr` | 🇨🇳 Çince | `zh` |
### README Çevirileri
| Dil | Dosya | Dil | Dosya |
|----------|------|----------|------|
| 🇺🇸 İngilizce | [README.md](../README.md) | 🇪🇸 İspanyolca | [README.es.md](README.es.md) |
| 🇸🇦 Arapça | [README.ar.md](README.ar.md) | 🇫🇷 Fransızca | [README.fr.md](README.fr.md) |
| 🇩🇪 Almanca | [README.de.md](README.de.md) | 🇮🇳 Hintçe | [README.hi.md](README.hi.md) |
| 🇮🇩 Endonezce | [README.id.md](README.id.md) | 🇮🇹 İtalyanca | [README.it.md](README.it.md) |
| 🇯🇵 Japonca | [README.ja.md](README.ja.md) | 🇵🇱 Lehçe | [README.pl.md](README.pl.md) |
| 🇧🇷 Portekizce | [README.pt.md](README.pt.md) | 🇷🇺 Rusça | [README.ru.md](README.ru.md) |
| 🇹🇷 Türkçe | [README.tr.md](README.tr.md) | 🇨🇳 Çince | [README.zh.md](README.zh.md) |
> 💡 **Çeviriye yardımcı olmak ister misiniz?** Daha fazla dil eklememize yardımcı olmak için [Katkıda Bulunma](#katkıda-bulunma) bölümüne bakın!
</details>
<a id="sorun-giderme"></a>
## 🛠️ Sorun Giderme
<details>
<summary>Yaygın sorunlar ve çözümler için tıklayın</summary>
- **Format tablosu görünmüyor:** yt-dlp'yi en son sürüme güncelleyin ve yt-dlp Nightly kanalına geçmeyi deneyin.
- **İndirme başarısız oldu:** İnternet bağlantınızı kontrol edin ve videonun erişilebilir olduğundan emin olun.
- **Belirli İndirme Hataları:**
- **Özel Videolar:** Özel içeriğe erişmek için çerez kimlik doğrulamasını kullanın.
- **Yaş Sınırlı İçerik:** Yaş sınırlı videoları görüntülemek için YouTube hesabınızda oturum açın.
- **Coğrafi Engelli Videolar:** Bölgesel kısıtlamaları aşmak için bir VPN kullanmayı düşünün.
- **Video Kaldırıldı:** Video artık YouTube'da mevcut değildir.
- **Canlı Yayınlar:** Canlı yayınlar yayınlanırken indirilemez; yayın bitene kadar bekleyin.
- **Ağ Hataları:** İnternet bağlantınızı kontrol edin ve tekrar deneyin.
- **Geçersiz URL:** URL'nin doğru olduğundan ve desteklenen bir platforma ait olduğundan emin olun.
- **Premium İçerik:** YouTube Premium aboneliği gerektirir.
- **Telif Hakkı Engeli:** İçerik telif hakkı kısıtlamaları nedeniyle engellenmiştir.
- **İndirme sonrası video ve ses dosyaları ayrı:** Bu durum FFmpeg eksik olduğunda veya algılanmadığında olur. YTSage, yüksek kaliteli video ve ses akışlarını birleştirmek için FFmpeg gerektirir.
- **Çözüm:** FFmpeg'in kurulu olduğundan ve sistem PATH'inizde erişilebilir olduğundan emin olun. Windows kullanıcıları için en kolay seçenek, FFmpeg ile birlikte gelen `YTSage-v<sürüm>-ffmpeg.exe` dosyasını indirmektir.
---
#### 🛡️ Windows Defender / Antivirüs Uyarısı
Bazı antivirüs yazılımları `.exe` dosyalarını yanlış pozitif olarak işaretleyebilir. Bu, paketlenmiş uygulamaların **bilinen bir sınırlamasıdır**.
**Neden olur:**
- Antivirüs sezgiselleri paketlenmiş yürütülebilir dosyaları hatalı bir şekilde şüpheli olarak tanımlayabilir.
**Güvenli Seçenekler:**
- ✅ **pip kurulumunu kullanın:** `pip install ytsage` (önerilir)
- ✅ **Kaynaktan derleyin**: Bu [kılavuzu](../.github/CI_CD_README.md) takip ederek
- ✅ **Uygulamayı antivirüs yazılımınızın beyaz listesine ekleyin**.
#### 🍎 macOS: "Uygulama hasarlı ve açılamıyor"
macOS Sonoma veya daha yeni sürümlerde bu hatayı görüyorsanız, karantina özniteliğini kaldırmanız gerekir.
1. **Terminal'i açın** (Spotlight kullanarak bulabilirsiniz).
2. **Aşağıdaki komutu yazın**, ancak henüz Enter tuşuna **BASMAYIN**. Sonundaki boşluğu eklediğinizden emin olun:
```bash
xattr -d com.apple.quarantine
```
3. **`YTSage.app` dosyasını Finder penceresinden sürükleyin** ve doğrudan Terminal penceresine bırakın. Bu, doğru dosya yolunu otomatik olarak yapıştıracaktır.
4. Komutu çalıştırmak için **Enter tuşuna basın**.
5. **YTSage.app'i tekrar açmayı deneyin.** Artık düzgün bir şekilde çalışmalıdır.
---
#### **Yapılandırma Konumu (Gelişmiş)**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="sponsor-olun"></a>
## 💖 Sponsor Olun
YTSage size zaman kazandırıyorsa, projeye sponsor olmayı düşünün. Sponsorluklar geliştirme süresini, tüm platformlarda test yapmayı ve gelecekteki iyileştirmeleri karşılamaya yardımcı olur.
- GitHub Sponsors: https://github.com/sponsors/oop7
- Sponsorluk bağlantısı uygulamadaki Hakkında diyaloğu üzerinden doğrudan mevcuttur.
[![Sponsor YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="katkıda-bulunma"></a>
## 👥 Katkıda Bulunma
Katkılarınız bekliyoruz! İşte nasıl yardımcı olabileceğiniz:
1. 🍴 Depoyu Fork'layın
2. 🌿 Özellik dalınızı oluşturun:
```bash
git checkout -b feature/AmazingFeature
```
3. 💾 Değişikliklerinizi commit'leyin:
```bash
git commit -m ' AmazingFeature Ekle'
```
4. 📤 Dalı push'layın:
```bash
git push origin feature/AmazingFeature
```
5. 🔄 Bir Pull Request açın
### 🌍 Çevirilerle Katkıda Bulunun
- İlgili yerelleştirilmiş README dosyasını güncelleyin (örn: `readme-translations/README.tr.md`)
- Uygulama dizelerini `ytsage/languages/<code>.json` dosyasını düzenleyerek senkronize tutun
- Diliniz eksikse, `README.md` dosyasından başlayın ve `README.<code>.md` dosyasını oluşturun
<details>
<summary>📂 Proje Yapısı</summary>
## YTSage - Proje Yapısı
Bu belge YTSage'in düzenli klasör yapısını detaylandırır.
### 📁 Proje Düzeni
```
YTSage/
├── 📁 .github/ # GitHub konfigürasyonları
│ ├── 📁 ISSUE_TEMPLATE/ # Sorun şablonları
│ │ └── 🐛-bug-report.md # Hata raporu şablonu
│ ├─── 📁 workflows/ # GitHub Actions iş akışları
│ │ ├── build-linux.yml # Linux derleme akışı
│ │ ├── build-macos.yml # macOS derleme akışı
│ │ │── build-windows.yml # Windows derleme akışı
| | └── release-all.yml # Ana yayın akışı
│ └── 📄 CI_CD_README.md # CI/CD dökümantasyonu
├── 📁 branding/ # Marka varlıkları (ekran görüntüleri, SVG'ler)
│ ├── 📁 icons/ # Uygulama ikonları
│ ├── 📁 screenshots/ # Dökümantasyon için ekran görüntüleri
│ └── 📁 svg/ # SVG varlıkları
├── 📄 LICENSE # Lisans dosyası
├── 📄 pyproject.toml # Proje metadatası ve bağımlılıklar
├── 📄 README.md # Proje dökümantasyonu
├── 📄 requirements.txt # Python bağımlılıkları (dev)
└── 📁 ytsage/ # Kaynak kod paketi
├── 📁 assets/ # Çalışma zamanı varlıkları
│ ├── 📁 Icon/ # Uygulama ikonları
│ └── 📁 sound/ # Ses dosyaları
├── 📁 languages/ # Yerelleştirme dosyaları
│ ├── 📄 ar.json # Arapça çeviri
│ ├── 📄 de.json # Almanca çeviri
│ ├── 📄 en.json # İngilizce çeviri
│ └── ... # Diğer diller
├── 📁 core/ # Temel iş mantığı
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_deno.py # Deno entegrasyonu
│ ├── 📄 ytsage_downloader.py # İndirme işlevselliği
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg entegrasyonu
│ ├── 📄 ytsage_utils.py # Yardımcı fonksiyonlar
│ └── 📄 ytsage_yt_dlp.py # yt-dlp entegrasyonu
├── 📁 gui/ # Kullanıcı arayüzü bileşenleri
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_gui_main.py # Ana uygulama penceresi
│ └── 📁 ytsage_gui_dialogs/ # Diyalog sınıfları
├── 📁 utils/ # Yardımcı modüller
│ ├── 📄 __init__.py
│ ├── 📄 ytsage_config_manager.py # Yapılandırma yönetimi
│ └── 📄 ytsage_logger.py # Günlük tutma araçları
├── 📄 __init__.py # Paket giriş noktası
└── 📄 main.py # Ana yürütme scripti
```
</details>
## ⭐️ Yıldız Geçmişi
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 Lisans
Bu proje MIT Lisansı altında lisanslanmıştır - detaylar için [LICENSE](../LICENSE) dosyasına bakın.
## 🙏 Teşekkürler
<details>
<summary>Teşekkürleri Göster</summary>
<div align="center">
<p>İyileştirmeler önermek veya hataları bildirmek için sorunlar açarak bu projeye katkıda bulunan herkese çok teşekkürler.</p>
<table>
<tr class="section"><th colspan="2">Temel Bileşenler</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>İndirme Motoru</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>Medya İşleme</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>yt-dlp entegrasyonu için runtime</td>
</tr>
<tr class="section"><th colspan="2">Kütüphaneler ve Frameworkler</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI Framework</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>Resim İşleme</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>HTTP İstekleri</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>Sürüm ve Paket Yönetimi</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown İşleme</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>Günlük Kaydı</td>
</tr>
<tr class="section"><th colspan="2">Varlıklar ve Katkıda Bulunanlar</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">Universfield'dan New Notification 09</a></td>
<td>Bildirim Sesi</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>Kod Katkıda Bulunan</td>
</tr>
</table>
</div>
</details>
## ⚠️ Feragatname
Bu araç sadece kişisel kullanım içindir. Lütfen YouTube'un Hizmet Şartlarına ve içerik oluşturucuların haklarına saygı gösterin.
---
<div align="center">
[oop7](https://github.com/oop7) tarafından ❤️ ile yapıldı
</div>
+576
View File
@@ -0,0 +1,576 @@
<div align="center">
<img src="../branding/svg/ytsage-wordmark.svg" width="400" alt="ytsage-wordmark">
<img src="../branding/screenshots/main.png" width="800" alt="YTSage 界面"/>
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-1f2937?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
[![PyPI 下载量](https://img.shields.io/pepy/dt/ytsage?color=1f2937&style=for-the-badge&label=downloads&logo=python&logoColor=white)](https://pepy.tech/project/ytsage)
[![GitHub 下载量](https://img.shields.io/github/downloads/oop7/YTSage/total?color=1f2937&style=for-the-badge&label=downloads&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![许可证: MIT](https://img.shields.io/badge/License-MIT-1f2937?style=for-the-badge&logo=opensource&logoColor=white)](https://opensource.org/licenses/MIT)
[![支持平台](https://img.shields.io/badge/platform-cross--platform-1f2937?style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/releases)
[![GitHub Stars](https://img.shields.io/github/stars/oop7/YTSage?color=c90000&style=for-the-badge&logo=github&logoColor=white)](https://github.com/oop7/YTSage/stargazers)
[![PyPI 版本](https://img.shields.io/pypi/v/ytsage?color=c90000&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ytsage/)
[![GitHub 赞助](https://img.shields.io/github/sponsors/oop7?color=c90000&style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/oop7)
**一款现代、整洁、基于 PySide6 的 YouTube 下载器。**
支持下载任意质量的视频、提取音频、获取字幕等更多功能。
### 🌍 README 语言
英语: [EN](../README.md)
| 阿拉伯语: [AR](README.ar.md)
| 德语: [DE](README.de.md)
| 西班牙语: [ES](README.es.md)
| 法语: [FR](README.fr.md)
| 印地语: [HI](README.hi.md)
| 印尼语: [ID](README.id.md)
| 意大利语: [IT](README.it.md)
| 日语: [JA](README.ja.md)
| 波兰语: [PL](README.pl.md)
| 葡萄牙语: [PT](README.pt.md)
| 俄语: [RU](README.ru.md)
| 土耳其语: [TR](README.tr.md)
| 中文: [ZH](README.zh.md)
<p align="center">
<a href="#安装">安装</a> •
<a href="#功能">功能</a> •
<a href="#使用说明">使用说明</a> •
<a href="#屏幕截图">屏幕截图</a> •
<a href="#故障排除">故障排除</a> •
<a href="#赞助支持">赞助支持</a> •
<a href="#贡献">贡献</a>
</p>
</div>
---
<a id="为什么选择-ytsage"></a>
## ❓ 为什么选择 YTSage?
YTSage 专为寻找 **简单但强大** 的 YouTube 下载器的用户而设计。与其他工具不同,它提供:
- 现代、整洁的 PySide6 用户界面
- 一键下载视频、音频和字幕
- 支持 SponsorBlock、字幕合并和播放列表选择等高级功能
- 可选的“通用模式”,支持 yt-dlp 兼容的其他非 YouTube 网站
- 跨平台支持,安装简单
<a id="功能"></a>
## ✨ 功能
<div align="center">
| 核心功能 | 高级功能 | 更多功能 |
|-----------------------------------|-----------------------------------------|------------------------------------|
| 🎥 格式选择列表 | 🚫 集成 SponsorBlock | 🎞️ FPS / HDR 显示 |
| 🎵 音频提取 | 📝 字幕选择与合并 | 🔄 yt-dlp 自动更新 |
| ✨ 现代 UI 体验 | 💾 保存描述与缩略图 | 🛠️ FFmpeg/yt-dlp/Deno 检测 |
| 📋 播放列表支持与选择 | 🚀 下载限速 | ⚙️ 自定义参数支持 |
| 📑 视频列表集成 | ✂️ 视频剪辑 | 🍪 Cookie 登录集成 |
| 📜 下载历史记录 | 🔄 更新版本分支选择 | 🌐 代理支持 |
| 🎚️ 音频格式转换 | 🎬 视频格式设置 | 🆙 内置更新页签 |
| 🌍 通用模式 | 🔊 音频归一化 (EBU R128) | 🌍 支持 14 种语言 |
| 💾 播放列表导出 | ⚙️ 默认质量与字幕设置 | |
</div>
<a id="安装"></a>
## 🚀 安装
### ⚡ 快速安装 (推荐)
通过 PyPI 安装 YTSage
```bash
pip install ytsage
```
<details>
<summary>🔄 更新现有安装</summary>
```bash
pip install --upgrade ytsage
```
</details>
然后通过以下命令运行:
```bash
ytsage
```
### 📦 独立可执行文件
> [👉 下载最新版本](https://github.com/oop7/YTSage/releases/latest)
#### 🪟 Windows
| 格式 | 说明 |
|--------|-------------|
| ![Windows EXE](https://img.shields.io/badge/Windows-EXE-0078D6?style=for-the-badge&logo=windows&logoColor=white) | 标准安装程序 |
| ![Windows FFmpeg](https://img.shields.io/badge/Windows-FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | 包含 FFmpeg 的安装程序 |
| ![Windows Portable](https://img.shields.io/badge/Windows-Portable-0078D6?style=for-the-badge&logo=windows&logoColor=white) | 便携版 (无需安装) |
| ![Windows Portable FFmpeg](https://img.shields.io/badge/Windows-Portable%20FFmpeg-0078D6?style=for-the-badge&logo=windows&logoColor=white) | 包含 FFmpeg 的便携版 (ZIP) |
<details>
<summary>🛠️ 安装步骤</summary>
1. **EXE 安装程序 (`.exe`)**: 双击并按照安装向导进行操作。
2. **便携版 (`.zip`)**: 解压到所需位置并运行 `ytsage.exe`
3. **内置 FFmpeg**: 如果系统没有安装 FFmpeg,请选择带 `-ffmpeg` 的版本。
</details>
#### 🐧 Linux
| 格式 | 说明 |
|--------|-------------|
| ![Linux DEB](https://img.shields.io/badge/Linux-DEB-FCC624?style=for-the-badge&logo=linux&logoColor=black) | Debian 软件包 |
| ![Linux AppImage](https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black) | 便携 AppImage |
| ![Linux RPM](https://img.shields.io/badge/Linux-RPM-FCC624?style=for-the-badge&logo=linux&logoColor=black) | RPM 软件包 |
| ![Flathub](https://img.shields.io/badge/Linux-Flatpak-FCC624?style=for-the-badge&logo=flathub&logoColor=black) | Flatpak 软件包 |
<details>
<summary>🛠️ 安装步骤</summary>
- **DEB (`.deb`)**:
```bash
sudo dpkg -i ytsage_*.deb
sudo apt-get install -f # 如有依赖问题请运行
```
- **RPM (`.rpm`)**:
```bash
sudo rpm -i ytsage-*.rpm
```
- **AppImage (`.AppImage`)**:
```bash
chmod +x YTSage-*.AppImage
./YTSage-*.AppImage
```
- **Flatpak**: 按照 Flathub 的说明或运行:
```bash
flatpak install flathub io.github.oop7.ytsage
```
</details>
#### 🍎 macOS
| 格式 | 说明 |
|--------|-------------|
| ![macOS ARM64 APP](https://img.shields.io/badge/macOS-ARM64%20APP-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon 专用 ZIP 压缩包 |
| ![macOS ARM64 DMG](https://img.shields.io/badge/macOS-ARM64%20DMG-000000?style=for-the-badge&logo=apple&logoColor=white) | Apple Silicon 专用 DMG 安装程序 |
<details>
<summary>🛠️ 安装步骤</summary>
- **DMG 安装程序 (`.dmg`)**: 双击挂载,然后将 `YTSage.app` 拖入 Applications 文件夹。
- **ZIP 应用包 (`.zip`)**: 解压并移动 `YTSage.app` 到 Applications 文件夹。
*注意:如果遇到“应用已损坏”的提示,请参阅下方的 macOS 解决办法。*
</details>
---
<details>
<summary>💻 从源码手动安装</summary>
### 1. 克隆仓库
```bash
git clone https://github.com/oop7/YTSage.git
cd YTSage
```
### 2. 安装依赖
#### ⚡ 使用 uv
```bash
uv pip install .
```
#### 📦 或使用标准 pip
```bash
pip install .
```
### 3. 运行程序
```bash
python -m ytsage.main
```
</details>
<a id="屏幕截图"></a>
## 📸 屏幕截图
<div align="center">
<table>
<tr>
<td><img src="../branding/screenshots/Download-Settings.png" alt="下载设置" width="400"/></td>
<td><img src="../branding/screenshots/playlist.png" alt="播放列表下载" width="400"/></td>
</tr>
<tr>
<td align="center"><em>下载设置</em></td>
<td align="center"><em>播放列表列表</em></td>
</tr>
<tr>
<td><img src="../branding/screenshots/audio_format.png" alt="音频格式选择" width="400"/></td>
<td><img src="../branding/screenshots/Custom-Option.png" alt="自定义选项" width="400"/></td>
</tr>
<tr>
<td align="center"><em>音频格式设置</em></td>
<td align="center"><em>自定义选项</em></td>
</tr>
</table>
</div>
<a id="使用说明"></a>
## 📖 使用说明
<details>
<summary>🎯 基本使用方法</summary>
1. **运行 YTSage**
2. **粘贴 YouTube 地址** (或点击 "Paste URL")
3. **点击 "Analyze" (分析)**
4. **选择下载模式:**
- `Video` 下载视频
- `Audio Only` 仅提取音频
5. **配置下载项:**
- 启用字幕并选择语言
- 启用字幕合并 (Merge)
- 保存缩略图 (Save Thumbnail)
- 启用赞助内容跳过 (SponsorBlock)
- 下载描述文本 (Save Description)
- 嵌入视频章节 (Embed Chapters)
6. **选择路径**
7. **点击 "Download" (下载)**
> 💡 默认下载到系统的“下载”文件夹。
</details>
<details>
<summary>📋 下载播放列表</summary>
1. **粘贴播放列表地址**
2. **添加并分析**
3. **在弹出窗口中选择视频 (默认全选)**
4. **设置质量方案**
5. **开始下载**
> 💡 程序会自动管理下载队列。您可以将列表导出为 `.txt`, `.csv`, `.m3u` 或 `.json` 格式。
</details>
<details>
<summary>🌍 非 YouTube 网站 (通用模式)</summary>
当您想从被 yt-dlp 支持的其他网站(如 TikTok、Dailymotion 等)下载时,请开启通用模式。
如何使用:
1. 打开 `Download Settings` (下载设置)。
2. 启用 `Generic Mode` (通用模式)。
3. 输入非 YouTube 的支持网站链接。
4. 点击 `Analyze` (分析)。
5. 正常下载。
说明:
- 通用模式仅关闭了 YTSage 内部的 URL 类型检查。网站支持程度仍取决于您安装的 yt-dlp。
- 有些站点可能需要特定的 Cookie 或代理配置。
- 如果某个网站无法分析,请先在内置更新程序中更新 yt-dlp。
</details>
<details>
<summary>🧰 媒体与下载选项</summary>
- **字幕选项:** 过滤特定语言并支持嵌入到视频中。
- **字幕合并:** 将字幕“烧录”到视频中(硬字幕)。
- **保存描述:** 将视频描述保存为独立的文本文档。
- **保存缩略图:** 下载视频的高清封面。
- **嵌入章节:** 允许将视频章节标记写入媒体元数据。
- **赞助商跳过:** 配合 SponsorBlock 自动跳过或剪辑掉广告段落。
- **视频剪裁:** 输入 `HH:MM:SS` 时间点来实现部分下载。
</details>
<details>
<summary>⚙️ 文件与输出设置</summary>
- **下载限速:** 输入如 `500K` 表示限制为 500 KB/s。
- **保存路径:** 在 **Download Settings → Download Path** 中保存您的默认位置。
- **默认分辨率:** 设置您首选的清晰度(如 1080p, 720p)。
- **默认字幕语言:** 输入语言代码(如 `zh,en`)来自动选择默认字幕。
- **文件命名模板:** 通过 `%(title)s` 等变量自定义文件名格式。
- **强制输出格式:** 强制转换输出容器,如 `mp4`, `webm` 或 `mkv`。
- **音频转换:** 将音频转换为 `AAC`, `MP3`, `FLAC` 等格式。
- **音量归一化:** 使用 EBU R128 标准使下载的音量均衡。
- **多线程连接:** 设置 **Concurrent Connections** 为 8-10 以最大化下载速度。
</details>
<details>
<summary>🌐 进阶访问与网络</summary>
- **Cookie 登录:** 允许通过 Cookie 下载私人内容或绕过限制。
推荐方法:
1. 在设置中点击 `Extract cookies from browser`,选择您的浏览器。
2. 或:导出 Netscape 格式的 `cookies.txt` 文件并手动载入。
- **代理支持:** 支持设置 HTTP 代理,例如 `http://127.0.0.1:8080`。
</details>
<details>
<summary>🛠️ 系统工具与维护</summary>
- **自定义参数:** 为 yt-dlp 传递特定的命令行参数。
- **内置更新器:** (在 Custom Options 中)
- **yt-dlp 更新:** 在稳定版和测试版之间切换。
- **FFmpeg 检测:** 验证安装路劲。
- **Deno 更新:** 维护相关的集成环境。
- **下载历史:** 管理您的所有下载历史记录,包含缩略图和状态。
</details>
<details>
<summary>🌍 语言支持</summary>
YTSage 支持 **14 种语言**。您可以在 **Custom Options → Language** 中更改。
### 支持的界面语言
| 语言 | 代码 | 语言 | 代码 |
|----------|------|----------|------|
| 🇺🇸 英语 | `en` | 🇪🇸 西班牙语 | `es` |
| 🇸🇦 阿拉伯语 | `ar` | 🇫🇷 法语 | `fr` |
| 🇩🇪 德语 | `de` | 🇮🇳 印地语 | `hi` |
| 🇮🇩 印尼语 | `id` | 🇮🇹 意大利语 | `it` |
| 🇯🇵 日语 | `ja` | 🇵🇱 波兰语 | `pl` |
| 🇧🇷 葡萄牙语 | `pt` | 🇷🇺 俄语 | `ru` |
| 🇹🇷 土耳其语 | `tr` | 🇨🇳 中文 | `zh` |
### README 翻译版
| 语言 | 文件 | 语言 | 文件 |
|----------|------|----------|------|
| 🇺🇸 英语 | [README.md](../README.md) | 🇪🇸 西班牙语 | [README.es.md](README.es.md) |
| 🇸🇦 阿拉伯语 | [README.ar.md](README.ar.md) | 🇫🇷 法语 | [README.fr.md](README.fr.md) |
| 🇩🇪 德语 | [README.de.md](README.de.md) | 🇮🇳 印地语 | [README.hi.md](README.hi.md) |
| 🇮🇩 印尼语 | [README.id.md](README.id.md) | 🇮🇹 意大利语 | [README.it.md](README.it.md) |
| 🇯🇵 日语 | [README.ja.md](README.ja.md) | 🇵🇱 波兰语 | [README.pl.md](README.pl.md) |
| 🇧🇷 葡萄牙语 | [README.pt.md](README.pt.md) | 🇷🇺 俄语 | [README.ru.md](README.ru.md) |
| 🇹🇷 土耳其语 | [README.tr.md](README.tr.md) | 🇨🇳 中文 | [README.zh.md](README.zh.md) |
> 💡 **想要贡献翻译?** 欢迎查看 [贡献指南](#贡献) 部分!
</details>
<a id="故障排除"></a>
## 🛠️ 故障排除
<details>
<summary>常见问题解答</summary>
- **没有显示格式表格:** 请更新 yt-dlp。如果仍无效,请尝试切换到测试版 (Nightly) 分支。
- **下载失败:** 请检查网络连接或该地区视频是否可用。
- **常见特定的错误提示:**
- **私有视频:** 需提供 Cookie 登录。
- **内容受限:** 部分视频需登录账号。
- **地理锁定:** 需使用代理或 VPN。
- **视频已删除:** 视频已不存在。
- **直播内容:** YTSage 目前不支持实时直播下载,请等直播结束后再试。
- **下载后画面和声音分离:** 意味着系统中未安装或未检测到 FFmpeg。
- **解决方法:** 确保 FFmpeg 已安装并加入 PATH,或下载带 `-ffmpeg` 版本的 Windows 程序。
---
#### 🛡️ Windows Defender / 杀毒软件警告
某些杀毒软件可能会误报。这是打包程序的常见现象。
**原因:**
- 启发式查杀可能会误认为打包的可执行文件是恶意软件。
**安全建议:**
- ✅ **通过 pip 安装:** `pip install ytsage`(推荐)
- ✅ **自行构建**: 参照 [CI_CD 指南](../.github/CI_CD_README.md)
- ✅ **添加排除项**。
#### 🍎 macOS: "应用已损坏,无法打开"
在 macOS Sonoma 及以后版本:
1. 打开 **终端 (Terminal)**。
2. 输入命令 (结尾有一个空格):
```bash
xattr -d com.apple.quarantine
```
3. 从 Finder 中将 **YTSage.app** 拖入终端窗口。
4. 按回车。
5. 再次打开应用。
---
#### **配置存放路径**
- **Windows:** `%LOCALAPPDATA%\YTSage`
- **macOS:** `~/Library/Application Support/YTSage`
- **Linux:** `~/.local/share/YTSage`
</details>
<a id="赞助支持"></a>
## 💖 赞助支持
如果 YTSage 节省了您的时间,请考虑资助本项目。赞助收入将用于多平台测试环境的维护和新功能开发。
- GitHub Sponsors: https://github.com/sponsors/oop7
- 您也可以通过应用内的“About (关于)”窗口找到捐赠链接。
[![赞助 YTSage](https://img.shields.io/badge/Sponsor-YTSage-EA4AAA?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/oop7)
<a id="贡献"></a>
## 👥 贡献
感谢所有帮助!
1. 🍴 Fork 仓库
2. 🌿 创建特性分支: `git checkout -b feature/NewFeature`
3. 💾 提交更改: `git commit -m 'Add NewFeature'`
4. 📤 推送分支: `git push origin feature/NewFeature`
5. 🔄 开启 Pull Request
### 🌍 翻译贡献
- 您可以更新各语种 README (例如 `readme-translations/README.zh.md`)。
- 也可以翻译界面词条:`ytsage/languages/<语言代码>.json`。
<details>
<summary>📂 项目结构</summary>
## YTSage - 项目结构
### 📁 目录概览
```
YTSage/
├── 📁 .github/ # GitHub 设置
│ ├── 📁 ISSUE_TEMPLATE/ # 问题模板
│ ├─── 📁 workflows/ # GitHub Actions 流程
├── 📁 branding/ # 品牌资源 (截图, SVG)
│ ├── 📁 icons/ # 图标
│ ├── 📁 screenshots/ # 截图说明
│ └── 📁 svg/ # SVG 素材
├── 📄 LICENSE # 许可证
├── 📄 pyproject.toml # 项目元数据与依赖
├── 📄 README.md # 英语 README
├── 📄 requirements.txt # 开发依赖
└── 📁 ytsage/ # 源代码
├── 📁 assets/ # 运行时资源 (音频, 图标)
├── 📁 languages/ # 多语言 JSON 文件
├── 📁 core/ # 下载与集成核心逻辑
├── 📁 gui/ # UI 组件
├── 📁 utils/ # 工具类
├── 📄 __init__.py
└── 📄 main.py # 程序入口
```
</details>
## ⭐️ 关注趋势
<div align="center">
## Star History
<a href="https://www.star-history.com/#oop7/YTSage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=oop7/YTSage&type=Date" />
</picture>
</a>
</div>
## 📜 许可证
本项目基于 MIT 许可证分发 - 详情请参阅 [LICENSE](../LICENSE) 文件。
## 🙏 鸣谢
<details>
<summary>查看致谢名单</summary>
<div align="center">
<p>特别鸣谢所有通过反馈、建议或代码合并来完善此工具的贡献者。</p>
<table>
<tr class="section"><th colspan="2">核心组件</th></tr>
<tr>
<td width="35%"><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></td>
<td>核心下载引擎</td>
</tr>
<tr>
<td><a href="https://ffmpeg.org/">FFmpeg</a></td>
<td>媒体流处理</td>
</tr>
<tr>
<td><a href="https://deno.com/">Deno</a></td>
<td>集成运行环境</td>
</tr>
<tr class="section"><th colspan="2">库与框架</th></tr>
<tr>
<td><a href="https://wiki.qt.io/Qt_for_Python">PySide6</a></td>
<td>GUI 框架</td>
</tr>
<tr>
<td><a href="https://python-pillow.org/">Pillow</a></td>
<td>图片处理</td>
</tr>
<tr>
<td><a href="https://requests.readthedocs.io/">requests</a></td>
<td>网络请求</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/">packaging</a></td>
<td>版本管理</td>
</tr>
<tr>
<td><a href="https://python-markdown.github.io/">markdown</a></td>
<td>Markdown 渲染</td>
</tr>
<tr>
<td><a href="https://github.com/Delgan/loguru">loguru</a></td>
<td>日志记录</td>
</tr>
<tr class="section"><th colspan="2">内容与贡献</th></tr>
<tr>
<td><a href="https://pixabay.com/sound-effects/new-notification-09-352705/">Universfield 的通知音效</a></td>
<td>通知声音</td>
</tr>
<tr>
<td><a href="https://github.com/viru185">viru185</a></td>
<td>代码贡献</td>
</tr>
</table>
</div>
</details>
## ⚠️ 免责声明
本工具仅供个人学习与研究使用。请尊重 YouTube 服务条款及创作者版权。
---
<div align="center">
由 [oop7](https://github.com/oop7) 倾力协作 ❤️
</div>
@@ -0,0 +1,418 @@
; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***
;
; To download user-contributed translations of this file, go to:
; https://jrsoftware.org/files/istrans/
;
; Note: When translating this text, do not add periods (.) to the end of
; messages that didn't have them already, because on those messages Inno
; Setup adds the periods automatically (appending a period would result in
; two periods being displayed).
;
; Maintained by Zhenghan Yang
; Email: 847320916@QQ.com
; Translation based on network resource
; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
;
[LangOptions]
; The following three entries are very important. Be sure to read and
; understand the '[LangOptions] section' topic in the help file.
LanguageName=简体中文
; If Language Name display incorrect, uncomment next line
; LanguageName=<7B80><4F53><4E2D><6587>
; About LanguageID, to reference link:
; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
LanguageID=$0804
; About CodePage, to reference link:
; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
LanguageCodePage=936
; If the language you are translating to requires special font faces or
; sizes, uncomment any of the following entries and change them accordingly.
;DialogFontName=
;DialogFontSize=9
;DialogFontBaseScaleWidth=7
;DialogFontBaseScaleHeight=15
;WelcomeFontName=Segoe UI
;WelcomeFontSize=14
[Messages]
; *** 应用程序标题
SetupAppTitle=安装
SetupWindowTitle=安装 - %1
UninstallAppTitle=卸载
UninstallAppFullTitle=%1 卸载
; *** Misc. common
InformationTitle=信息
ConfirmTitle=确认
ErrorTitle=错误
; *** SetupLdr messages
SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
HelpTextNote=
; *** 启动错误消息
LastErrorMessage=%1。%n%n错误 %2: %3
SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
InvalidParameter=无效的命令行参数:%n%n%1
SetupAlreadyRunning=安装程序正在运行。
WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
NotOnThisPlatform=此程序不能在 %1 上运行。
OnlyOnThisPlatform=此程序只能在 %1 上运行。
OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。
SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
; *** 启动问题
PrivilegesRequiredOverrideTitle=选择安装程序模式
PrivilegesRequiredOverrideInstruction=选择安装模式
PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
PrivilegesRequiredOverrideText2=%1 可以仅为您安装,或为所有用户安装(需要管理员权限)。
PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)
PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)
PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项)
; *** 其他错误
ErrorCreatingDir=安装程序无法创建目录“%1”
ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
; *** 安装程序公共消息
ExitSetupTitle=退出安装程序
ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
AboutSetupMenuItem=关于安装程序(&A)...
AboutSetupTitle=关于安装程序
AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
AboutSetupNote=
TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
; *** 按钮
ButtonBack=< 上一步(&B)
ButtonNext=下一步(&N) >
ButtonInstall=安装(&I)
ButtonOK=确定
ButtonCancel=取消
ButtonYes=是(&Y)
ButtonYesToAll=全是(&A)
ButtonNo=否(&N)
ButtonNoToAll=全否(&O)
ButtonFinish=完成(&F)
ButtonBrowse=浏览(&B)...
ButtonWizardBrowse=浏览(&R)...
ButtonNewFolder=新建文件夹(&M)
; *** “选择语言”对话框消息
SelectLanguageTitle=选择安装语言
SelectLanguageLabel=选择安装时使用的语言。
; *** 公共向导文字
ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
BeveledLabel=
BrowseDialogTitle=浏览文件夹
BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
NewFolderName=新建文件夹
; *** “欢迎”向导页
WelcomeLabel1=欢迎使用 [name] 安装向导
WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。
; *** “密码”向导页
WizardPassword=密码
PasswordLabel1=这个安装程序有密码保护。
PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
PasswordEditLabel=密码(&P)
IncorrectPassword=您输入的密码不正确,请重新输入。
; *** “许可协议”向导页
WizardLicense=许可协议
LicenseLabel=请在继续安装前阅读以下重要信息。
LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
LicenseAccepted=我同意此协议(&A)
LicenseNotAccepted=我不同意此协议(&D)
; *** “信息”向导页
WizardInfoBefore=信息
InfoBeforeLabel=请在继续安装前阅读以下重要信息。
InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
WizardInfoAfter=信息
InfoAfterLabel=请在继续安装前阅读以下重要信息。
InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
; *** “用户信息”向导页
WizardUserInfo=用户信息
UserInfoDesc=请输入您的信息。
UserInfoName=用户名(&U)
UserInfoOrg=组织(&O)
UserInfoSerial=序列号(&S)
UserInfoNameRequired=您必须输入用户名。
; *** “选择目标目录”向导页
WizardSelectDir=选择目标位置
SelectDirDesc=您想将 [name] 安装在哪里?
SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share
InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
DiskSpaceWarningTitle=磁盘空间不足
DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
DirNameTooLong=文件夹名称或路径太长。
InvalidDirName=文件夹名称无效。
BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
DirExistsTitle=文件夹已存在
DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
DirDoesntExistTitle=文件夹不存在
DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
; *** “选择组件”向导页
WizardSelectComponents=选择组件
SelectComponentsDesc=您想安装哪些程序组件?
SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
FullInstallation=完全安装
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
CompactInstallation=简洁安装
CustomInstallation=自定义安装
NoUninstallWarningTitle=组件已存在
NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
; *** “选择附加任务”向导页
WizardSelectTasks=选择附加任务
SelectTasksDesc=您想要安装程序执行哪些附加任务?
SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
; *** “选择开始菜单文件夹”向导页
WizardSelectProgramGroup=选择开始菜单文件夹
SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
MustEnterGroupName=您必须输入一个文件夹名。
GroupNameTooLong=文件夹名或路径太长。
InvalidGroupName=无效的文件夹名字。
BadGroupName=文件夹名不能包含下列任何字符:%n%n%1
NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
; *** “准备安装”向导页
WizardReady=准备安装
ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。
ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
ReadyLabel2b=点击“安装”继续此安装程序。
ReadyMemoUserInfo=用户信息:
ReadyMemoDir=目标位置:
ReadyMemoType=安装类型:
ReadyMemoComponents=已选择组件:
ReadyMemoGroup=开始菜单文件夹:
ReadyMemoTasks=附加任务:
; *** TExtractionWizardPage 向导页面与 ExtractArchive
ExtractingLabel=正在解压文件...
ButtonStopExtraction=停止解压(&S)
StopExtraction=您确定要停止解压吗?
ErrorExtractionAborted=解压已中止
ErrorExtractionFailed=解压失败:%1
; *** 压缩文件解压失败详情
ArchiveIncorrectPassword=压缩文件密码不正确
ArchiveIsCorrupted=压缩文件已损坏
ArchiveUnsupportedFormat=不支持的压缩文件格式
; *** TDownloadWizardPage 向导页面和 DownloadTemporaryFile
DownloadingLabel2=正在下载文件...
ButtonStopDownload=停止下载(&S)
StopDownload=您确定要停止下载吗?
ErrorDownloadAborted=下载已中止
ErrorDownloadFailed=下载失败:%1 %2
ErrorDownloadSizeFailed=获取下载大小失败:%1 %2
ErrorProgress=无效的进度:%1 / %2
ErrorFileSize=文件大小错误:预期 %1,实际 %2
; *** “正在准备安装”向导页
WizardPreparing=正在准备安装
PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。
PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。
CannotContinue=安装程序不能继续。请点击“取消”退出。
ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
CloseApplications=自动关闭应用程序(&A)
DontCloseApplications=不要关闭应用程序(&D)
ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动?
; *** “正在安装”向导页
WizardInstalling=正在安装
InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。
; *** “安装完成”向导页
FinishedHeadingLabel=[name] 安装完成
FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。
FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
ClickFinish=点击“完成”退出安装程序。
FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗?
FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗?
ShowReadmeCheck=是,我想查阅自述文件
YesRadio=是,立即重启电脑(&Y)
NoRadio=否,稍后重启电脑(&N)
; used for example as 'Run MyProg.exe'
RunEntryExec=运行 %1
; used for example as 'View Readme.txt'
RunEntryShellExec=查阅 %1
; *** “安装程序需要下一张磁盘”提示
ChangeDiskTitle=安装程序需要下一张磁盘
SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
PathLabel=路径(&P)
FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
SelectDirectoryLabel=请指定下一张磁盘的位置。
; *** 安装阶段消息
SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
AbortRetryIgnoreSelectAction=选择操作
AbortRetryIgnoreRetry=重试(&T)
AbortRetryIgnoreIgnore=忽略错误并继续(&I)
AbortRetryIgnoreCancel=关闭安装程序
RetryCancelSelectAction=选择操作
RetryCancelRetry=重试(&T)
RetryCancelCancel=取消(&C)
; *** 安装状态消息
StatusClosingApplications=正在关闭应用程序...
StatusCreateDirs=正在创建目录...
StatusExtractFiles=正在提取文件...
StatusDownloadFiles=正在下载文件...
StatusCreateIcons=正在创建快捷方式...
StatusCreateIniEntries=正在创建 INI 条目...
StatusCreateRegistryEntries=正在创建注册表条目...
StatusRegisterFiles=正在注册文件...
StatusSavingUninstall=正在保存卸载信息...
StatusRunProgram=正在完成安装...
StatusRestartingApplications=正在重启应用程序...
StatusRollback=正在撤销更改...
; *** 其他错误
ErrorInternal2=内部错误:%1
ErrorFunctionFailedNoCode=%1 失败
ErrorFunctionFailed=%1 失败;错误代码 %2
ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
ErrorExecutingProgram=无法执行文件:%n%1
; *** 注册表错误
ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
; *** INI 错误
ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
; *** 文件复制错误
FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)
FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)
SourceIsCorrupted=源文件已损坏
SourceDoesntExist=源文件“%1”不存在
SourceVerificationFailed=源文件验证失败: %1
VerificationSignatureDoesntExist=签名文件“%1”不存在
VerificationSignatureInvalid=签名文件“%1”无效
VerificationKeyNotFound=签名文件“%1”使用了未知密钥
VerificationFileNameIncorrect=文件名不正确
VerificationFileTagIncorrect=文件标签不正确
VerificationFileSizeIncorrect=文件大小不正确
VerificationFileHashIncorrect=文件哈希值不正确
ExistingFileReadOnly2=无法替换现有文件,它是只读的。
ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
ErrorReadingExistingDest=尝试读取现有文件时出错:
FileExistsSelectAction=选择操作
FileExists2=文件已经存在。
FileExistsOverwriteExisting=覆盖已存在的文件(&O)
FileExistsKeepExisting=保留现有的文件(&K)
FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
ExistingFileNewerSelectAction=选择操作
ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)
ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
ErrorCreatingTemp=尝试在目标目录创建文件时出错:
ErrorReadingSource=尝试读取下列源文件时出错:
ErrorCopying=尝试复制下列文件时出错:
ErrorDownloading=下载文件时出错:
ErrorExtracting=解压压缩文件时出错:
ErrorReplacingExistingFile=尝试替换现有文件时出错:
ErrorRestartReplace=重启并替换失败:
ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
ErrorRegisterServer=无法注册 DLL/OCX%1
ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
ErrorRegisterTypeLib=无法注册类库:%1
; *** 卸载显示名字标记
; used for example as 'My Program (32-bit)'
UninstallDisplayNameMark=%1 (%2)
; used for example as 'My Program (32-bit, All users)'
UninstallDisplayNameMarks=%1 (%2, %3)
UninstallDisplayNameMark32Bit=32 位
UninstallDisplayNameMark64Bit=64 位
UninstallDisplayNameMarkAllUsers=所有用户
UninstallDisplayNameMarkCurrentUser=当前用户
; *** 安装后错误
ErrorOpeningReadme=尝试打开自述文件时出错。
ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。
; *** 卸载消息
UninstallNotFound=文件“%1”不存在。无法卸载。
UninstallOpenError=文件“%1”不能被打开。无法卸载。
UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)
ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。
UninstalledAll=已顺利从您的电脑中移除 %1。
UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗?
UninstallDataCorrupted=文件“%1”已损坏。无法卸载
; *** 卸载状态消息
ConfirmDeleteSharedFileTitle=删除共享的文件吗?
ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。
SharedFileNameLabel=文件名:
SharedFileLocationLabel=位置:
WizardUninstalling=卸载状态
StatusUninstalling=正在卸载 %1...
; *** Shutdown block reasons
ShutdownBlockReasonInstallingApp=正在安装 %1。
ShutdownBlockReasonUninstallingApp=正在卸载 %1。
; The custom messages below aren't used by Setup itself, but if you make
; use of them in your scripts, you'll want to translate them.
[CustomMessages]
NameAndVersion=%1 版本 %2
AdditionalIcons=附加快捷方式:
CreateDesktopIcon=创建桌面快捷方式(&D)
CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
ProgramOnTheWeb=%1 网站
UninstallProgram=卸载 %1
LaunchProgram=运行 %1
AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
AutoStartProgramGroupDescription=启动:
AutoStartProgram=自动启动 %1
AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
@@ -0,0 +1,336 @@
; *** Inno Setup version 5.5.3+ Hindi messages ***
; Translated by Him Prasad Gautam [ drishtibachak at gmail.com ]
; To download user-contributed translations of this file, go to:
; http://www.jrsoftware.org/files/istrans/
;
; Note: When translating this text, do not add periods (.) to the end of
; messages that didn't have them already, because on those messages Inno
; Setup adds the periods automatically (appending a period would result in
; two periods being displayed).
[LangOptions]
; The following three entries are very important. Be sure to read and
; understand the '[LangOptions] section' topic in the help file.
LanguageName=<0939><093F><0902><0926><0940>
LanguageID=$0439
LanguageCodePage=0
; If the language you are translating to requires special font faces or
; sizes, uncomment any of the following entries and change them accordingly.
;DialogFontName=
;DialogFontSize=10
;WelcomeFontName=
WelcomeFontSize=12
;TitleFontName=
TitleFontSize=35
;CopyrightFontName=
CopyrightFontSize=9
[Messages]
; *** Application titles
SetupAppTitle=स्थापना
SetupWindowTitle=स्थापना - %1
UninstallAppTitle=निस्कासन
UninstallAppFullTitle=%1 कि निस्कासन
; *** Misc. common
InformationTitle=सुचना
ConfirmTitle=पुष्टिकरण
ErrorTitle=त्रुटी
; *** SetupLdr messages
SetupLdrStartupMessage=इस से %1 आपकि कल्पयन्त्र में अधिष्ठापन होगा. क्या आप आगे बढ़ना चाहते है?
LdrCannotCreateTemp=अस्थाई फ़ाइल नही बना पा रहा. स्थापना को बिच में ही रोकना पड़ा.
LdrCannotExecTemp=अस्थाई फोल्डर में से फ़ाइल कार्यान्वयन नही कर पाया. स्थापना को बिच में ही रोकना पड़ा.
; *** Startup error messages
LastErrorMessage=%1.%n%nत्रुटी %2: %3
SetupFileMissing=फ़ाइल %1 अधिष्ठापन सङ्ग्रहिका में नही है. कृपया या तो समस्या का निदान कीजिये या कार्यक्रम की नई प्रति लाइए.
SetupFileCorrupt=स्थापना फाइल में त्रुटी है. कृपया नई कार्यक्रम की प्रति लाइए.
SetupFileCorruptOrWrongVer=स्थापना फाइल में त्रुटी है या तो अलग प्रकार कि है. कृपया समस्या-निदान करे या कार्यक्रम की नई प्रति लाइए.
InvalidParameter=फोल्डर का नाम वैध नही है.
SetupAlreadyRunning=स्थापना तो पहले से हि चल रहा है
WindowsVersionNotSupported=इस से [name/ver] आपके कल्पयन्त्र में अधिष्ठापन होगा.%n%nये बहेतर होगा आगे बढने से पहेले आप अन्य सभी कार्यक्रम हाल तुरत के लिए बंध कर दे.
WindowsServicePackRequired=ये कार्यक्रम को %1 Service Pack %2 या पिछला संस्करण चाहिए.
NotOnThisPlatform=ये कार्यक्रम %1 पे नही चलेगा.
OnlyOnThisPlatform=ये कार्यक्रम केवल %1 पे ही चलेगा.
OnlyOnTheseArchitectures=ये कार्यक्रम केवल इन प्रोसेसर :%n%n%1 से अनुरूप विन्डोज़ प्लेटफॉर्म पे ही चलेगा.
MissingWOW64APIs=आपका विंडो प्लेटफॉर्म 64-bit अधिष्ठापन समर्थन नही करता. कृपया सर्विस पैक %1 अधिष्ठापन करे.
WinVersionTooLowError=ये कार्यक्रम चलने के लिए %1 संस्करण %2 या उस से पिछला चाहिए.
WinVersionTooHighError=ये कार्यक्रम नही अधिष्ठापन किया जा सकता %1 संस्करण %2 या पिछला पे.
AdminPrivilegesRequired=अगर आप प्रशासक खाते से आरम्भ करे तो ही ये कार्यक्रम अधिष्ठापन कर पाओगे.
PowerUserPrivilegesRequired=आप प्रशासक खाते या शक्ति-प्रयोग कर्ता समूह के खाते से आरम्भ करे तो ही ये कार्यक्रम अधिष्ठापन कर पाओगे.
SetupAppRunningError=स्थापना ने पकड़ा की %1 हाल चालू है..%n%n कृपया उसे बंध करे अभी, और बाद में आगे बढने वास्ते ठीक या निकल जाने वास्ते रद्द करेँ पे क्लिक करे.
UninstallAppRunningError=निस्कासन को ये ज्ञात हुआ की %1 अभी चालू है.%n%n कृपया उसे बंध करे और फिर आगे बढने के लिए ठीक या बाहर जाने के लिए रद्द करेँ पे क्लिक करे.
; *** Misc. errors
ErrorCreatingDir=स्थापना "%1" सङ्ग्रहिका बनाने में विफल रहा
ErrorTooManyFilesInDir=%1 सङ्ग्रहिका में बहुत फाइल मौजूद होने के वजह से स्थापना फाइल बनाने में विफल रहा.
; *** Setup common messages
ExitSetupTitle=स्थापना कि बहिर्गमन
ExitSetupMessage=स्थापना कि कार्य पूर्ण नही हुआ, यदि आप अभी बाहर जाने कि ईराधा करेंगे तो कार्यक्रम सहि ढंग से अधिष्ठापन नही होगा.%n%nआप किसी ओर वक्त फिर से अधिष्ठापन कर सकते हो.%n%nक्या बाहर जाए?
AboutSetupMenuItem=स्थापना के बारे में...
AboutSetupTitle=स्थापना के बारे में
AboutSetupMessage=%1 संस्करण %2%n%3%n%n%1 गृह पृष्ठ:%n%4
AboutSetupNote=
TranslatorNote= यह हिन्दी में अनुवाद कि कार्य हिम प्रसाद गौतम ने किया है.
; *** Buttons
ButtonBack=< &पिछे हटो
ButtonNext=&आगे बढो >
ButtonInstall=&अधिष्ठापन
ButtonOK=&ठीक
ButtonCancel=&रद्द करेँ
ButtonYes=&हाँ
ButtonYesToAll=&सभी के लिए हाँ
ButtonNo=&नही
ButtonNoToAll=स&भी के लिए नही
ButtonFinish=&समाप्त
ButtonBrowse=&ब्राउज़...
ButtonWizardBrowse=&ब्राउज़...
ButtonNewFolder=&नया फोल्डर बनाए
; "Select Language" dialog messages
SelectLanguageTitle=स्थापना भाषा चयन
SelectLanguageLabel=अधिष्ठापन के दरम्यान इस्तेमाल होने वाली भाषा चयन करे:
; *** Common wizard text
ClickNext=आगे बढने के लिए आगे बढो दबाए, या बाहर जाने के वास्ते रद्द करेँ दबाए.
BeveledLabel= सौजन्यः हिम प्रसाद गौतम
BrowseDialogTitle=फोल्डर के लिए ब्राउज़ करे
BrowseDialogLabel=नीचे की सुची में से एक फोल्डर चयन करके ठीक दबाए.
NewFolderName=नया फोल्डर
; "Welcome" wizard page
WelcomeLabel1=यह [name] कि स्थापना हो रही समारोह में आपका स्वागत है
WelcomeLabel2=इस से [name/ver] आपके कल्पयन्त्र में अधिष्ठापन होगा.%n%nये बहेतर होगा आगे बढने से पहले आप अन्य सभी खुलि हुई कार्यक्रम हाल के लिए बंध कर दे.
; "Password" wizard page
WizardPassword=खुपियाशब्द
PasswordLabel1=ये अधिष्ठापन खुपियाशब्द से लोक है.
PasswordLabel3=कृपया खुपियाशब्द लिखें और बाद में 'आगे बढो' बटन दबाए. खुपियाशब्द case-सम्वेदनसील है.
PasswordEditLabel=खुपियाशब्द:
IncorrectPassword=आपने लिखा हुआ खुपियाशब्द गलत है. कृपया फिर से कोशिश करे.
; "License Agreement" wizard page
WizardLicense=इजाजत करार
LicenseLabel=आगे बढने से पहेले ये महत्वपूर्ण सूचनाए पढे.
LicenseLabel3=ये इजाजत करार पढे. आगे बढने से पहेले आपको इसकी शर्तों को मानना ही होगा.
LicenseAccepted=हाँ मुझे ये करारनामा कबूल है.
LicenseNotAccepted=नही मुझे ये करारनामा कबूल नही है.
; "Information" wizard pages
WizardInfoBefore=सुचना
InfoBeforeLabel=आगे बढने से पहेले ये महत्वपूर्ण सूचनाए पढे.
InfoBeforeClickLabel=जब आप तयार हो, 'आगे बढो' बटन दबाए.
WizardInfoAfter=सुचना
InfoAfterLabel=आगे बढने से पहेले ये महत्वपूर्ण सूचनाए पढे.
InfoAfterClickLabel=जब आप तयार हो, 'आगे बढो' बटन दबाए.
; "User Information" wizard page
WizardUserInfo=प्रयोग कर्ता की जानकारी
UserInfoDesc=कृपया आपकी जानकारी अंदर डाले.
UserInfoName=प्रयोग कर्ता का नाम:
UserInfoOrg=संस्था:
UserInfoSerial=क्रमाङ्क
UserInfoNameRequired=आपको नाम तो डालना ही होगा.
; "Select Destination Location" wizard page
WizardSelectDir=लक्ष्य पथ चयन करे
SelectDirDesc=[name] को किधर अधिष्ठापन करना है?
SelectDirLabel3=स्थापना [name] को निम्नलिखित फोल्डर में डालेगा.
SelectDirBrowseLabel=आगे बढने वास्ते आगे बढो दबाए. यदि अन्य फोल्डर चयन करना है तो ब्राउज़ दबाए.
DiskSpaceMBLabel=कमसेकम [mb] MB जितनी जगह तो जरूरी होगी.
CannotInstallToNetworkDrive=श्थापना ने नेटवर्क ड्राइभ नहि रख पाया.
CannotInstallToUNCPath=स्थापना ने UNC path नहि रख पाया.
InvalidPath=आपको ड्राइव अक्षर के साथ पूर्ण पथ देना होगा उदाहरण:%n%nC:\APP%n%n या तो UNC रास्ता यह रूप में:%n%n\\server\share
InvalidDrive=जो drive या UNC share आपने चयन की है उस मे हम पहुँच नही कर पा रहे कृपया अन्य चयन करे.
DiskSpaceWarningTitle=जरूरी जगह नही है.
DiskSpaceWarning=स्थापना कम से कम %1 KB जगह मंगता है, लेकिन चयनित ड्राइव में तो केवल %2 KB ही मौजूद है.%n%nक्या आप फिर भी आगे बढ़ना चाहते हो?
DirNameTooLong=फोल्डर का नाम या पथ बहोत लंबा है.
InvalidDirName=फोल्डर का नाम वैध नही है.
BadDirName32=फोल्डर नाम में ये अक्षर नही इस्तेमाल कर सकते:%n%n%1
DirExistsTitle=फोल्डर मौजूद है
DirExists=फोल्डर:%n%n%1%n%nपहेले से ही मौजूद है, क्या आप फिर भी उसमे अधिष्ठापन करना चाहते है?
DirDoesntExistTitle=फोल्डर मौजूद नही है
DirDoesntExist=फोल्डर:%n%n%1%n%nमौजूद नही है. क्या आप ये फोल्डर बनाना चाहते है?
; "Select Components" wizard page
WizardSelectComponents=सहयोगियोँ पसंद करे.
SelectComponentsDesc=कोनसे सहयोगियोँ अधिष्ठापन करने है?
SelectComponentsLabel2=जो सहयोगियोँ अधिष्ठापन करना है, उन्हें चयन करे; जिन्हें नही करना हो तो उन्हें साफ करे. जब आगे बढने के लिए तयार हो तो आगे बढो दबाए.
FullInstallation=सम्पूर्ण अधिष्ठापन
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
CompactInstallation=मजबुत अधिष्ठापन
CustomInstallation=रिवाजी अधिष्ठापन.
NoUninstallWarningTitle=सहयोगियोँ मौजूद है.
NoUninstallWarning=स्थापना को ये ज्ञात हुआ है की निम्नलिखित सहयोगियोँ पहेले से ही मोजूद है.:%n%n%1%n%nइन्हें डी-चयन करने से वे निस्कासन नही होगे.%n%nक्या आप ऐसे ही आगे बढ़ना चाहते है?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
ComponentsDiskSpaceMBLabel=इस चयन के साथ स्थापना वास्ते [mb] MB जगह चाहिए.
; "Select Additional Tasks" wizard page
WizardSelectTasks=अतिरिक्त काम चयन करे.
SelectTasksDesc=कोन से अतिरिक्त काम करने है?
SelectTasksLabel2=[name] को अधिष्ठापन करते वक्त जो अतिरिक्त काम करने है उन्हें चयन करे और बाद में आगे बढो पे क्लिक करे.
; "Select Start Menu Folder" wizard page
WizardSelectProgramGroup=सुरु मेनू फोल्डर चयन करे.
SelectStartMenuFolderDesc=कार्यक्रम के छोटीरास्ता किधर रखने है?
SelectStartMenuFolderLabel3=स्थापना कार्यक्रम के छोटीरास्ता निम्नलिखित सुरु-मेनू फोल्डर में डालेगा.
SelectStartMenuFolderBrowseLabel=आगे बढने के लिए आगे बढो दबाए. यदि अलग फोल्डर में अधिष्ठापन करना है तो Browse दबाए.
MustEnterGroupName=आपको फोल्डर का नाम तो डालना ही होगा.
GroupNameTooLong=फोल्डर का नाम या पथ बहुत लंबा है.
InvalidGroupName=फोल्डर का नाम वैध नही है.
BadGroupName=फोल्डर नाम में ये वाले अक्षर नही डाल सकते:%n%n%1
NoProgramGroupCheck2=सुरु मेनू फोल्डर नही बनाना है.
; "Ready to Install" wizard page
WizardReady=अधिष्ठापन के लिए तयार
ReadyLabel1=स्थापना अब [name] को आपके कल्पयन्त्रमें अधिष्ठापन करने के लिए तयार है.
ReadyLabel2a=आगे बढने के लिए अधिष्ठापन दबाए, अगर कोई बदलाव करना है तो पिछे हटो दबाए.
ReadyLabel2b=अधिष्ठापन में आगे बढने के लिए अधिष्ठापन दबाए.
ReadyMemoUserInfo=प्रयोग कर्ता की सूचनाए:
ReadyMemoDir=लक्ष्य सङ्ग्रहिका:
ReadyMemoType=स्थापना का प्रकार:
ReadyMemoComponents=चयन किये सहयोगियों:
ReadyMemoGroup=सुरु मेनू फोल्डर:
ReadyMemoTasks=अतिरिक्त काम:
; "Preparing to Install" wizard page
WizardPreparing=अधिष्ठापन के लिए तैयारी कर रहा है.
PreparingDesc=स्थापना [name] को आपके कल्पयन्त्र में डालने की तैयारीकर रहा है.
PreviousInstallNotCompleted=पिछले कार्यक्रम का प्रतिस्थापन / अधिष्ठापन सही ढंग से पूरा नही हुआ था. आपको कल्पयन्त्र फिर सुरु करना पडेगा.%n%nकल्पयन्त्र फिर सुरु करने पश्चात आप फिर से [name] का अधिष्ठापन शुरू करे.
CannotContinue=स्थापना आगे नही बढ़ सकता, कृपया रद्द करेँ बटन दबाएँ.
ApplicationsFound=निचे वाली अनुप्रयोगो ने स्थापना द्वारा अपडेट किया जाने वाला फाइलों को इस्तेमाल किया है. आपको यह मसवरा दिया जा ता है कि आप स्थापना को यह अनुप्रयोगौं कि खुद ही बन्द करने कि इजाजत प्रदान करें.
ApplicationsFound2=यह अनुप्रयोगो ने स्थापना द्वारा अपडेट किया जाने वाला फाइलों को इस्तेमाल किया है. आपको यह मसवरा दिया जा ता है कि आप स्थापना को यह अनुप्रयोगौं को खुद ही बन्द करने कि इजाजत प्रदान करें. अधिष्ठापन खतम होने के वाद, स्थापना यह अनुप्रयोग कों फिर सुरु करने कि कोसिस करेगा.
CloseApplications=&खुद हि अनुप्रयोग कों बन्द करें
DontCloseApplications=अनुप्रयोग कों बन्द &नहि करें
ErrorCloseApplications=स्थापना खुद हि सभी अनुप्रयोगों को बन्द नहि कर सका. आपको यह मसवरा दिया जाता है कि स्थापना ने अपडेट करने वाली फाइलों को इस्तमाल कर रहे अनुप्रयोगौं को आगे बढ्ने से पहले आप खुद ही बन्द करें.
; "Installing" wizard page
WizardInstalling=अधिष्ठापन हो रहा है.
InstallingLabel=जब तक स्थापना आपके कल्पयन्त्र में [name] अधिष्ठापन करता है, उस वख्त तक कृपया प्रतीक्षा करे.
; "Setup Completed" wizard page
FinishedHeadingLabel=[name] स्थापना कि कार्य पूरा हो रहा है.
FinishedLabelNoIcons=स्थापना ने [name] को आपके कल्पयन्त्र में सफलतापूर्वक अधिष्ठापन कर दिया है.
FinishedLabel=स्थापना ने [name] आपके कल्पयन्त्रमें अधिष्ठापन कर दिया है. आप उपयुक्त प्रतिमा पे क्लिक कर के कभी भी ये कार्यक्रम शुरू कर सकते है.
ClickFinish=स्थापना से बाहर निकलने वास्ते समाप्त पे क्लिक करे.
FinishedRestartLabel=[name] का अधिष्ठापन पूरा करने वास्ते कल्पयन्त्र फिर सुरु करना बेहद जरूरी है. %n%nक्या आप अभी रिसुरु करना चाहते है?
FinishedRestartMessage=[name] का अधिष्ठापन पूरा करने हेतु कल्पयन्त्र फिर सुरु करना बेहद जरूरी है.%n%nक्या आप अभी फिर सुरु करना चाहते है?
ShowReadmeCheck=हाँ मुझे हमें पढो file देखनी है.
YesRadio=&हाँ, कल्पयन्त्र फिर सुरु कर दो.
NoRadio=&नही मै अपना कल्पयन्त्र स्वयं बाद में फिर सुरु करूँगा.
; used for example as 'Run MyProg.exe'
RunEntryExec=रन %1
; used for example as 'View Readme.txt'
RunEntryShellExec=देखे %1
; "Setup Needs the Next Disk" stuff
ChangeDiskTitle=स्थापना के लिए अगली डिस्क चाहिए.
SelectDiskLabel2=कृपया डिस्क %1 डालके ठीक दबाए.%n%nयदि इस डिस्क की फाइल नही मिलती तो सही पथ बताए या ब्राउज़ पे क्लिक करे.
PathLabel=पथ:
FileNotInDir2=फाइल "%1" को "%2" में ढुढ नही पाए. कृपया सही डिस्क डाले या अलग फोल्डर चयन करे.
SelectDirectoryLabel=अगली डिस्क का पता बताए.
; *** Installation phase messages
SetupAborted=स्थापना पूरा नही हो पाया.%n%nकृपया त्रुटी ठीक करे और फिर से प्रयास करे.
EntryAbortRetryIgnore=फिर से प्रयास करने वास्ते Retry दबाए, यदि ऐसे ही आगे बढ़ना है तो Ignore दबाए, या तो स्थापना रद्द करेँ करने वास्ते Abort दबाए.
; *** Installation status messages
StatusClosingApplications=अनुप्रयोगकों बन्द किया जा रहा है.
StatusCreateDirs=सङ्ग्रहिका बना रहा है...
StatusExtractFiles=फाइल उत्खनन कर रहा है...
StatusCreateIcons=छोटीरास्ता बना रहा है...
StatusCreateIniEntries=INI एंट्री बना रहा है...
StatusCreateRegistryEntries=पञ्जीका एंट्री बना रहा है...
StatusRegisterFiles=फाइल पञ्जिकृत कर रहा है...
StatusSavingUninstall=निस्कासन की सुचनाए बचतकर रहा है...
StatusRunProgram=अधिष्ठापन पूरा कर रहा है...
StatusRestartingApplications=अनुप्रयोगकों कि फिर सुरुवात
StatusRollback=बदलावों को पिछे हट्ने कि काम कर रहा है...
; *** Misc. errors
ErrorInternal2=आंतरिक त्रुटी: %1
ErrorFunctionFailedNoCode=%1 विफल
ErrorFunctionFailed=%1 विफल; कोड %2
ErrorFunctionFailedWithMessage=%1 विफल; कोड %2.%n%3
ErrorExecutingProgram=फाइल को कार्यान्वयन नही कर पा रहा:%n%1
; *** Registry errors
ErrorRegOpenKey=पञ्जीका कुञ्जी खोलते वक्त त्रुटी:%n%1\%2
ErrorRegCreateKey=पञ्जीका कुञ्जी बनाते वक्त त्रुटी:%n%1\%2
ErrorRegWriteKey=पञ्जीका कुञ्जी में लिखते वक्त त्रुटी:%n%1\%2
; *** INI errors
ErrorIniEntry=फ़ाइल "%1" में INI एंट्री डालते वक्त त्रुटी.
; *** File copying errors
FileAbortRetryIgnore=फिर से प्रयास करने हेतु Retry बटन दबाए, यदि ऐसे ही आगे बढ़ना है तो Ignore दबाए(हम ऐसा सुजाव नही देते),या तो Abort दबाए अधिष्ठापन को रद्द करेँ करने हेतु.
FileAbortRetryIgnore2=फिर से प्रयास करने हेतु Retry बटन दबाए, यदि ऐसे ही आगे बढ़ना है तो Ignore दबाए(हम ऐसा सुजाव नही देते),या तो Abort दबाए अधिष्ठापन को रद्द करेँ करने हेतु.
SourceIsCorrupted=श्रोत फ़ाइल में गडबड है.
SourceDoesntExist=श्रोत फाइल "%1" मौजूद ही नही है.
ExistingFileReadOnly=मौजदा फ़ाइल सिर्फ-पढो है.%n%n आप Retry पे क्लिक करे, उसका सिर्फ-पढो attribute हटाने के लिए और फिर दोबारा प्रयास करे. यदि इस फाइल को छोड़ देना है तो Ignore, और यदि अधिष्ठापन रद्द करेँ करना है तो Abort बटन दबाए.
ErrorReadingExistingDest=मौजदा फाइल को पढते वक्त त्रुटी:
FileExists=फाइल पहेले से मौजूद है.%n%nक्या आप उसको ओवर-राईट करना चाहते हो?
ExistingFileNewer=मौजूदा फाइल स्थापना फ़ाइल से नई है. हमारा सुजाव है की आप इसे रखे.%n%nक्या आप फ़ाइल को रखना चाहते है?
ErrorChangingAttr=मौजूदा फाइल के एट्रीब्यूट बदलते वक्त त्रुटी:
ErrorCreatingTemp=फ़ाइल बनाते वख्त त्रुटी:
ErrorReadingSource=श्रोत फाइल खोलते वक्त त्रुटी:
ErrorCopying=फ़ाइल प्रति करने का प्रयास करते वक्त त्रुटी:
ErrorReplacingExistingFile=मौजूद फाइल को प्रतिस्थापना करते वक्त त्रुटी:
ErrorRestartReplace=प्रतिस्थापन कि फिर से सुरुवात विफल रहा:
ErrorRenamingTemp=सङ्ग्रहिका में फाइल का नाम बदलते वक्त त्रुटी हुई:
ErrorRegisterServer=इस को पञ्जिकृत नही कर पा रहा DLL/OCX: %1
ErrorRegSvr32Failed=RegSvr32 असफल हो गयी, बाहर जाने कोड %1 के साथ
ErrorRegisterTypeLib=इस टाइप लाइब्रेरी को पंजीकृत नही कर पा रहा: %1
; *** Post-installation errors
ErrorOpeningReadme=मुझे पढो फ़ाइल खोलते वक्त त्रुटी हुई.
ErrorRestartingComputer=स्थापना कल्पयन्त्र को फिर सुरु करने में असफल रहा. कृपया आप ही इसको फिर सुरु करे.
; *** Uninstaller messages
UninstallNotFound=फाइल "%1" मौजूद ही नही है. निस्कासन करना असंभव.
UninstallOpenError=फ़ाइल "%1" खुल नही रही. निस्कासन करना असंभव.
UninstallUnsupportedVer=निस्कासन लोग फ़ाइल "%1" जिस फोर्मेट में है उसे हम पहचान नही पा रहे. आगे बढ़ना नामुमकिन.
UninstallUnknownEntry=निस्कासन लोग में एक अज्ञात प्रविष्टी (%1)मिली.
ConfirmUninstall=क्या पक्का आप %1 को निस्कासन करना चाहते हो?
UninstallOnlyOnWin64=केवल 64-bit Windows से ही इसे निस्कासन किया जा सकता है.
OnlyAdminCanUninstall=केवल प्रशासक खातों से ही इसे निस्कासन किया जा सकता है..
UninstallStatusLabel=जब तक %1 नही हड्ता, धैर्य रखे.
UninstalledAll=%1 सफलतापूर्वक निस्कासन हुआ.
UninstalledMost=%1 निस्कासन पूरा हुआ.%n%nकुछ तत्वों को निकाल नही पाए लेकिन आप उन्हें अपनि तरह से हटा सकते हो.
UninstalledAndNeedsRestart=%1 का निस्कासन पूरा करने वास्ते कल्पयन्त्र को फिर सुरु करना जरूरी है.%n%nक्या अभी फिर सुरु करे?
UninstallDataCorrupted=%1 फ़ाइल में त्रुटी. निस्कासन नामुमकिन.
; *** Uninstallation phase messages
ConfirmDeleteSharedFileTitle=क्या शेरेड-फाइल को निकाल देना है?
ConfirmDeleteSharedFile2=प्रणाली से ये ज्ञात होता है की निम्नलिखिती शेरेड-फ़ाइल अब आगे इस्तेमाल में नही आएगी. क्या आप उन्हें भी निस्कासन करना चाहते है?%n%n यदि कोई अन्य कार्यक्रम इन फाइल पे आधारित है तो वो शायद इन्हें निकाल देने पर ढंग से काम ना भी करे. यदि आप फैसला नही कर पा रहे तो 'नही' पे क्लिक करे. इन फाइल को कल्पयन्त्र में पड़े रहेने दोगे तो भी कोई नुकसान नही होगा.
SharedFileNameLabel=फाइल नाम:
SharedFileLocationLabel=पता:
WizardUninstalling=निस्कासन स्थिति
StatusUninstalling=निस्कासन हो रहा है %1...
; *** Shutdown block reasons
ShutdownBlockReasonInstallingApp= %1 कि अधिष्ठआपन हो रही है.
ShutdownBlockReasonUninstallingApp=%1 कि निस्कासन हो रही है.
; The custom messages below aren't used by Setup itself, but if you make
; use of them in your scripts, you'll want to translate them.
[CustomMessages]
NameAndVersion=%1 संस्करण %2
AdditionalIcons=अतिरिक्त प्रतिमा:
CreateDesktopIcon=डेस्कटॉप प्रतिमा बनाए
CreateQuickLaunchIcon=जल्दि चलो प्रतिमा बनाए
ProgramOnTheWeb=%1 इन्टरनेट पे
UninstallProgram=निस्कासन करे %1
LaunchProgram=लोंच करे %1
AssocFileExtension=%1 को %2 फ़ाइल एक्सटेंशन के साथ आबद्ध करे
AssocingFileExtension=%1 को %2 फ़ाइल एक्सटेंशन के साथ आबद्ध कर रहा है....
AutoStartProgramGroupDescription=सुरुवात
AutoStartProgram=%1 को %2 फ़ाइल एक्सटेंशन के साथ आबद्ध कर रहा है....
AddonHostProgramNotFound=आपने चयन किया हुआफोल्डर में %1 नही मिला. %n%nक्या आप किसि हालत में यस कि निरन्तरता रख्ना चाहते है ?
@@ -0,0 +1,350 @@
; *** Inno Setup version 6.4.0+ Indonesian messages ***
;
; Untuk mengunduh terjemahan kontribusi-pengguna dari berkas ini, buka:
; http://www.jrsoftware.org/files/istrans/
;
; Alih bahasa oleh: MozaikTM (mozaik.tm@gmail.com)
[LangOptions]
LanguageName=Bahasa Indonesia
LanguageID=$0421
LanguageCodePage=0
[Messages]
SetupAppTitle=Instalasi
SetupWindowTitle=Instalasi - %1
UninstallAppTitle=Pelepas
UninstallAppFullTitle=Pelepasan %1
InformationTitle=Informasi
ConfirmTitle=Konfirmasi
ErrorTitle=Galat
SetupLdrStartupMessage=Kami akan memasang %1. Teruskan?
LdrCannotCreateTemp=Tidak dapat membuat berkas sementara. Batal memasang
LdrCannotExecTemp=Tidak dapat menjalankan berkas di direktori sementara. Batal memasang
LastErrorMessage=%1.%n%nGalat %2: %3
SetupFileMissing=Berkas %1 hilang dari direktori instalasi. Silakan koreksi masalah atau dapatkan salinan program yang baru.
SetupFileCorrupt=Berkas pemandu telah rusak. Silakan dapatkan salinan program yang baru.
SetupFileCorruptOrWrongVer=Berkas pemandu telah rusak, atau tidak cocok dengan versi pemandu ini. Silakan koreksi masalah atau dapatkan salinan program yang baru.
InvalidParameter=Parameter tak sah terdapat pada baris perintah: %n%n%1
SetupAlreadyRunning=Pemandu sudah berjalan.
WindowsVersionNotSupported=Program ini tidak mendukung versi Windows yang berjalan pada komputer Anda.
WindowsServicePackRequired=Program ini memerlukan %1 Service Pack %2 atau yang terbaru.
NotOnThisPlatform=Program ini tidak akan berjalan pada %1.
OnlyOnThisPlatform=Program ini harus dijalankan pada %1.
OnlyOnTheseArchitectures=Program ini hanya bisa dipasang pada versi Windows yang didesain untuk arsitektur prosesor berikut:%n%n%1
WinVersionTooLowError=Program ini memerlukan %1 versi %2 atau yang terbaru.
WinVersionTooHighError=Program ini tidak dapat dipasang pada %1 versi %2 atau yang terbaru.
AdminPrivilegesRequired=Anda harus masuk sebagai seorang administrator saat memasang program ini.
PowerUserPrivilegesRequired=Anda harus masuk sebagai seorang administrator atau anggota grup Power Users saat memasang program ini.
SetupAppRunningError=Kami mendeteksi bahwa %1 sedang berjalan.%n%nSilakan tutup semua instansi bersangkutan, lalu klik OK untuk meneruskan, atau Cancel untuk keluar.
UninstallAppRunningError=Pelepas mendeteksi bahwa %1 sedang berjalan.%n%nSilakan tutup semua instansi bersangkutan, lalu klik OK untuk meneruskan, atau Cancel untuk keluar.
;Inno6
PrivilegesRequiredOverrideTitle=Pilih Mode Instalasi
PrivilegesRequiredOverrideInstruction=Pilih mode instalasi
PrivilegesRequiredOverrideText1=%1 bisa dipasang untuk semua pengguna (perlu izin administratif), atau hanya Anda.
PrivilegesRequiredOverrideText2=%1 bisa dipasang hanya untuk Anda, atau semua pengguna (perlu izin administratif).
PrivilegesRequiredOverrideAllUsers=Pasang untuk &semua pengguna
PrivilegesRequiredOverrideAllUsersRecommended=Pasang untuk &semua pengguna (disarankan)
PrivilegesRequiredOverrideCurrentUser=Pasang &hanya untuk saya
PrivilegesRequiredOverrideCurrentUserRecommended=Pasang &hanya untuk saya (disarankan)
;Inno6
ErrorCreatingDir=Kami tidak dapat membuat direktori "%1"
ErrorTooManyFilesInDir=Tidak dapat membuat berkas di direktori "%1" karena berisi terlalu banyak berkas
ExitSetupTitle=Keluar Pemandu
ExitSetupMessage=Instalasi tidak lengkap. Bila Anda keluar sekarang, program takkan terpasang.%n%nAnda bisa menjalankan Pemandu lagi lain kali untuk melengkapinya.%n%nKeluar?
AboutSetupMenuItem=&Tentang Pemandu...
AboutSetupTitle=Tentang Pemandu
AboutSetupMessage=%1 versi %2%n%3%n%n%1 laman beranda:%n%4
AboutSetupNote=
TranslatorNote=
ButtonBack=&Kembali
ButtonNext=&Maju
ButtonInstall=&Pasang
ButtonOK=Oke
ButtonCancel=Batal
ButtonYes=&Ya
ButtonYesToAll=Y&a semuanya
ButtonNo=&Tidak
ButtonNoToAll=T&idak semuanya
ButtonFinish=&Selesai
ButtonBrowse=&Cari...
ButtonWizardBrowse=C&ari...
ButtonNewFolder=&Buat Map Baru
SelectLanguageTitle=Pilih Bahasa Pemandu
SelectLanguageLabel=Pilih bahasa untuk digunakan ketika memasang.
ClickNext=Klik Maju untuk meneruskan, atau Batal untuk keluar.
BeveledLabel=
BrowseDialogTitle=Cari Map
BrowseDialogLabel=Pilih map dari daftar berikut, lalu klik OK.
NewFolderName=Map Baru
WelcomeLabel1=Selamat datang di Pemandu Instalasi [name]
WelcomeLabel2=Kami akan memasang [name/ver] pada komputer Anda.%n%nDisarankan untuk menutup semua aplikasi lainnya sebelum meneruskan.
WizardPassword=Kata Sandi
PasswordLabel1=Instalasi ini dilindungi kata sandi.
PasswordLabel3=Silakan masukkan kata sandi, lalu klik Maju untuk meneruskan. Kata sandi bersifat sensitif-kapitalisasi.
PasswordEditLabel=&Kata Sandi:
IncorrectPassword=Kata sandi yang Anda masukkan salah. Silakan coba lagi.
WizardLicense=Kesepakatan Lisensi
LicenseLabel=Silakan baca informasi berikut sebelum meneruskan.
LicenseLabel3=Silakan baca Kesepakatan Lisensi berikut. Anda harus setuju dengan syarat dari kesepakatan ini sebelum meneruskan instalasi.
LicenseAccepted=Saya &setujui kesepakatan ini
LicenseNotAccepted=Saya &tidak setuju kesepakatan ini
WizardInfoBefore=Informasi
InfoBeforeLabel=Silakan baca informasi penting berikut sebelum meneruskan.
InfoBeforeClickLabel=Saat Anda siap meneruskan instalasi, klik Maju.
WizardInfoAfter=Informasi
InfoAfterLabel=Silakan baca informasi penting berikut sebelum meneruskan.
InfoAfterClickLabel=Saat Anda siap meneruskan instalasi, klik Maju.
WizardUserInfo=Informasi Pengguna
UserInfoDesc=Silakan masukkan informasi Anda.
UserInfoName=&Nama Pengguna:
UserInfoOrg=&Organisasi:
UserInfoSerial=&Nomor Seri:
UserInfoNameRequired=Wajib memasukkan nama.
WizardSelectDir=Pilih Lokasi Tujuan
SelectDirDesc=Di manakah [name] sebaiknya dipasang?
SelectDirLabel3=Kami akan memasang [name] ke dalam map berikut.
SelectDirBrowseLabel=Untuk meneruskan, klik Maju. Bila Anda ingin memilih map lain, klik Cari.
;Inno6
DiskSpaceGBLabel=Diperlukan sedikitnya [gb] GB ruang bebas.
;Inno6
DiskSpaceMBLabel=Diperlukan sedikitnya [mb] MB ruang bebas.
CannotInstallToNetworkDrive=Kami tidak bisa memasang ke diska jaringan.
CannotInstallToUNCPath=Kami tidak bisa memasang ke alamat UNC.
InvalidPath=Anda wajib memasukkan alamat lengkap dengan huruf diska; contohnya:%n%nC:\APP%n%natau alamat UNC dalam bentuk:%n%n\\server\share
InvalidDrive=Diska atau alamat UNC yang Anda pilih tidak ada atau tidak dapat diakses. Silakan pilih yang lain.
DiskSpaceWarningTitle=Ruang Bebas Tidak Cukup
DiskSpaceWarning=Kami memerlukan sedikitnya %1 KB ruang bebas untuk memasang, namun diska yang Anda pilih hanya memiliki %2 KB tersedia.%n%nMaju terus?
DirNameTooLong=Nama map atau alamat terlalu panjang.
InvalidDirName=Nama map tidak sah.
BadDirName32=Nama map dilarang berisi karakter-karakter berikut:%n%n%1
DirExistsTitle=Map Sudah Ada
DirExists=Map:%n%n%1%n%nsudah ada. Tetap pasang di map tersebut?
DirDoesntExistTitle=Map Tidak Ada
DirDoesntExist=Map:%n%n%1%n%ntidak ada. Buat map?
WizardSelectComponents=Pilih Komponen
SelectComponentsDesc=Komponen mana sajakah yang sebaiknya dipasang?
SelectComponentsLabel2=Centang komponen yang Anda inginkan; hapus centang dari komponen yang tidak Anda inginkan. Klik Maju saat Anda siap meneruskan.
FullInstallation=Instalasi penuh
CompactInstallation=Instalasi padat
CustomInstallation=Instalasi kustom
NoUninstallWarningTitle=Komponen Terpasang
NoUninstallWarning=Kami mendeteksi bahwa komponen berikut telah terpasang pada komputer Anda:%n%n%1%n%nMembatalkan pilihan atas komponen berikut bukan berarti melepasnya.%n%nMaju terus?
ComponentSize1=%1 KB
ComponentSize2=%1 MB
;Inno6
ComponentsDiskSpaceGBLabel=Pilihan saat ini memerlukan sedikitnya [gb] GB ruang bebas.
;Inno6
ComponentsDiskSpaceMBLabel=Pilihan saat ini memerlukan sedikitnya [mb] MB ruang bebas.
WizardSelectTasks=Pilih Tugas Tambahan
SelectTasksDesc=Tugas tambahan mana sajakah yang sebaiknya dijalankan?
SelectTasksLabel2=Pilih tugas tambahan yang Anda ingin kami jalankan ketika memasang [name], lalu klik Maju.
WizardSelectProgramGroup=Pilih Map Menu Start
SelectStartMenuFolderDesc=Di manakah sebaiknya kami letakkan pintasan program?
SelectStartMenuFolderLabel3=Kami akan membuat pintasan program di map Menu Start berikut.
SelectStartMenuFolderBrowseLabel=Untuk meneruskan, klik Maju. Bila Anda ingin memilih map lain, klik Cari.
MustEnterGroupName=Anda wajib memasukkan nama map.
GroupNameTooLong=Nama map atau alamat terlalu panjang.
InvalidGroupName=Nama map tidak sah.
BadGroupName=Nama map dilarang berisi karakter-karakter berikut:%n%n%1
NoProgramGroupCheck2=&Jangan buat map Menu Start
WizardReady=Siap Memasang
ReadyLabel1=Kami siap untuk memulai instalasi [name] pada komputer Anda.
ReadyLabel2a=Klik Pasang untuk meneruskan instalasi, atau klik Kembali bila Anda ingin menilik atau mengubah setelan.
ReadyLabel2b=Klik Pasang untuk meneruskan instalasi.
ReadyMemoUserInfo=Informasi pengguna:
ReadyMemoDir=Lokasi tujuan:
ReadyMemoType=Tipe instalasi:
ReadyMemoComponents=Komponen terpilih:
ReadyMemoGroup=Map Menu Start:
ReadyMemoTasks=Tugas Tambahan:
;Inno6
DownloadingLabel=Mengunduh berkas tambahan...
ButtonStopDownload=&Setop Unduhan
StopDownload=Anda yakin ingin berhenti mengunduh?
ErrorDownloadAborted=Unduhan dibatalkan
ErrorDownloadFailed=Gagal mengunduh: %1 %2
ErrorDownloadSizeFailed=Gagal mendapatkan ukuran: %1 %2
ErrorFileHash1=Ceksum berkas gagal: %1
ErrorFileHash2=Ceksum berkas tidak sah: seharusnya %1, yang kami dapatkan %2
ErrorProgress=Langkah tidak sah: %1 dari %2
ErrorFileSize=Ukuran berkas tidak sah: seharusnya %1, yang kami dapatkan %2
; *** TExtractionWizardPage wizard page and Extract7ZipArchive
ExtractionLabel=Mengektrasi berkas tambahan...
ButtonStopExtraction=&Hentikan ekstrasi
StopExtraction=Anda yakin ingin menghentikan ekstrasi?
ErrorExtractionAborted=Ekstrasi dibatalkan
ErrorExtractionFailed=Ekstraksi gagal: %1
;Inno6
WizardPreparing=Bersiap Memasang
PreparingDesc=Kami sedang bersiap memasang [name] pada komputer Anda.
PreviousInstallNotCompleted=Instalasi/pelepasan dari program sebelumnya tidak lengkap. Anda perlu memulai ulang komputer untuk melengkapinya nanti.%n%nSetelah itu, jalankan Pemandu kembali untuk melengkapi instalasi [name].
CannotContinue=Kami tidak bisa meneruskan. Klik Batal untuk keluar.
ApplicationsFound=Aplikasi berikut tengah memakai berkas-berkas yang perlu kami perbarui. Disarankan agar Anda mengizinkan kami untuk menutupnya secara otomatis.
ApplicationsFound2=Aplikasi berikut tengah memakai berkas-berkas yang perlu kami perbarui. Disarankan agar Anda mengizinkan kami untuk menutupnya secara otomatis. Selengkapnya memasang, kami akan berusaha memulai ulang aplikasi-aplikasi tersebut.
CloseApplications=&Otomatis tutup aplikasi
DontCloseApplications=&Jangan tutup aplikasi
ErrorCloseApplications=Kami tidak dapat menutup semua aplikasi secara otomatis. Disarankan agar Anda menutup semua aplikasi yang memakai berkas-berkas yang perlu kami perbarui sebelum meneruskan.
;Inno6
PrepareToInstallNeedsRestart=Kami perlu memulai ulang komputer Anda. Setelah itu, jalankan Pemandu kembali untuk melengkapi pemasangan [name].%n%nMulai ulang sekarang?
;Inno6
WizardInstalling=Memasang
InstallingLabel=Silakan tunggu selagi kami memasang [name] pada komputer Anda.
FinishedHeadingLabel=Mengakhiri Instalasi [name]
FinishedLabelNoIcons=Kami telah selesai memasang [name] pada komputer Anda.
FinishedLabel=Kami telah selesai memasang [name] pada komputer Anda. Aplikasi tersebut bisa dijalankan dengan cara memilih pintasan yang terpasang.
ClickFinish=Klik Selesai untuk menutup instalasi.
FinishedRestartLabel=Demi melengkapi instalasi [name], kami perlu memulai ulang komputer Anda. Lakukan sekarang?
FinishedRestartMessage=Demi melengkapi instalasi [name], kami perlu memulai ulang komputer Anda.%n%nLakukan sekarang?
ShowReadmeCheck=Ya, saya ingin melihat berkas README
YesRadio=&Ya, mulai ulang komputer sekarang
NoRadio=&Tidak, saya akan memulai ulang komputer nanti
RunEntryExec=Jalankan %1
RunEntryShellExec=Lihat %1
ChangeDiskTitle=Kami Memerlukan Diska Sambungan
SelectDiskLabel2=Silakan masukkan Diska %1 dan klik OK.%n%nBila berkas-berkas di dalam diska ini dapat ditemukan di map lain selain yang ditampilkan di bawah, masukkan alamat yang benar atau klik Cari.
PathLabel=&Alamat:
FileNotInDir2=Berkas "%1" tidak dapat ditemukan di "%2". Silakan masukkan diska yang benar atau pilih map lain.
SelectDirectoryLabel=Silakan tentukan lokasi diska berikutnya.
SetupAborted=Instalasi tidak lengkap.%n%nSilakan koreksi masalah dan jalankan Pemandu kembali.
;Inno6
AbortRetryIgnoreSelectAction=Pilih tindakan
AbortRetryIgnoreRetry=&Coba lagi
AbortRetryIgnoreIgnore=&Abaikan galat dan teruskan
AbortRetryIgnoreCancel=Batalkan pemasangan
;Inno6
StatusClosingApplications=Menutup aplikasi...
StatusCreateDirs=Membuat direktori...
StatusExtractFiles=Mengekstrak berkas...
StatusCreateIcons=Membuat pintasan...
StatusCreateIniEntries=Membuat catatan INI...
StatusCreateRegistryEntries=Membuat catatan Registry...
StatusRegisterFiles=Meregistrasi berkas...
StatusSavingUninstall=Menyimpan informasi pelepas...
StatusRunProgram=Mengakhiri instalasi...
StatusRestartingApplications=Menjalankan ulang aplikasi...
StatusRollback=Membatalkan perubahan...
ErrorInternal2=Galat internal: %1
ErrorFunctionFailedNoCode=%1 gagal
ErrorFunctionFailed=%1 gagal; kode %2
ErrorFunctionFailedWithMessage=%1 gagal; kode %2.%n%3
ErrorExecutingProgram=Tidak dapat mengeksekusi berkas:%n%1
ErrorRegOpenKey=Galat membuka kunci Registry:%n%1\%2
ErrorRegCreateKey=Galat membuat kunci Registry:%n%1\%2
ErrorRegWriteKey=Galat menulis kunci Registry:%n%1\%2
ErrorIniEntry=Galat membuat catatan INI dalam berkas "%1".
;Inno6
FileAbortRetryIgnoreSkipNotRecommended=&Lewati berkas ini (tidak disarankan)
FileAbortRetryIgnoreIgnoreNotRecommended=&Abaikan galat dan teruskan (tidak disarankan)
;Inno6
SourceIsCorrupted=Berkas asal telah rusak
SourceDoesntExist=Berkas asal "%1" tidak ada
;Inno6
ExistingFileReadOnly2=Berkas yang sudah ada tidak bisa ditimpa karena telah ditandai hanya-baca.
ExistingFileReadOnlyRetry=&Hapus atribut hanya-baca dan coba lagi
ExistingFileReadOnlyKeepExisting=&Pertahankan berkas yang sudah ada
ErrorReadingExistingDest=Terjadi galat saat berusaha membaca berkas yang sudah ada:
FileExistsSelectAction=Pilih tindakan
FileExists2=Berkas sudah ada.
FileExistsOverwriteExisting=&Timpa berkas yang sudah ada
FileExistsKeepExisting=&Pertahankan berkas yang sudah ada
FileExistsOverwriteOrKeepAll=&Lakukan ini untuk konflik (bentrok) berikutnya
ExistingFileNewerSelectAction=Pilih tindakan
ExistingFileNewer2=Berkas yang sudah ada lebih baru dari yang akan kami coba pasang.
ExistingFileNewerOverwriteExisting=&Timpa berkas yang sudah ada
ExistingFileNewerKeepExisting=&Pertahankan berkas yang sudah ada (disarankan)
ExistingFileNewerOverwriteOrKeepAll=&Lakukan ini untuk konflik (bentrok) berikutnya
;Inno6
ErrorReadingExistingDest=Terjadi galat saat berusaha membaca berkas yang sudah ada:
ErrorChangingAttr=Terjadi galat saat berusaha mengubah atribusi berkas yang sudah ada:
ErrorCreatingTemp=Terjadi galat saat berusaha membuat berkas di direktori tujuan:
ErrorReadingSource=Terjadi galat saat berusaha membaca berkas asal:
ErrorCopying=Terjadi galat saat berusaha menyalin berkas:
ErrorReplacingExistingFile=Terjadi galat saat berusaha menimpa berkas yang sudah ada:
ErrorRestartReplace=RestartReplace gagal:
ErrorRenamingTemp=Terjadi galat saat berusaha mengubah nama berkas di direktori tujuan:
ErrorRegisterServer=Tidak dapat meregistrasi DLL/OCX: %1
ErrorRegSvr32Failed=RegSvr32 gagal dengan kode akhir %1
ErrorRegisterTypeLib=Tidak dapat meregistrasi berkas referensi: %1
;Inno6
UninstallDisplayNameMark=%1 (%2)
UninstallDisplayNameMarks=%1 (%2, %3)
UninstallDisplayNameMark32Bit=32-bita
UninstallDisplayNameMark64Bit=64-bita
UninstallDisplayNameMarkAllUsers=Semua pengguna
UninstallDisplayNameMarkCurrentUser=Pengguna saat ini
;Inno6
ErrorOpeningReadme=Terjadi galat saat berusaha membuka berkas README.
ErrorRestartingComputer=Kami gagal memulai ulang komputer. Silakan lakukan secara manual.
UninstallNotFound=Berkas "%1" tidak ada. Tidak bisa melepas
UninstallOpenError=Berkas "%1" tidak dapat dibuka. Tidak bisa melepas
UninstallUnsupportedVer=Berkas catatan pelepas "%1" tidak dalam format yang kami kenali. Tidak bisa melepas
UninstallUnknownEntry=Entri tak dikenal (%1) ditemukan dalam catatan pelepas
ConfirmUninstall=Anda yakin ingin melepas %1 beserta semua komponennya?
UninstallOnlyOnWin64=Instalasi ini hanya bisa dilepas pada Windows 64-bita.
OnlyAdminCanUninstall=Instalasi ini hanya bisa dilepas oleh pengguna dengan izin administratif.
UninstallStatusLabel=Silakan tunggu selagi %1 dihapus dari komputer Anda.
UninstalledAll=%1 berhasil dihapus dari komputer Anda.
UninstalledMost=Selesai melepas %1.%n%nBeberapa elemen tidak dapat dihapus. Anda bisa menghapusnya secara manual.
UninstalledAndNeedsRestart=Untuk melengkapi pelepasan %1, komputer Anda perlu dimulai ulang.%n%nMulai ulang sekarang?
UninstallDataCorrupted=Berkas "%1" rusak. Tidak bisa melepas
ConfirmDeleteSharedFileTitle=Hapus Berkas Bersama?
ConfirmDeleteSharedFile2=Sistem mengindikasi bahwa berkas bersama di bawah ini tidak lagi dipakai oleh program mana pun. Apa Anda ingin agar kami menghapusnya?%n%nBila masih ada program yang memakainya dan berkas ini dihapus, program tersebut dapat tidak berfungsi dengan semestinya. Bila Anda ragu, pilih No. Membiarkan berkas ini pada sistem Anda takkan membahayakan.
SharedFileNameLabel=Nama berkas:
SharedFileLocationLabel=Lokasi:
WizardUninstalling=Status Pelepasan
StatusUninstalling=Melepas %1...
ShutdownBlockReasonInstallingApp=Memasang %1.
ShutdownBlockReasonUninstallingApp=Melepas %1.
[CustomMessages]
NameAndVersion=%1 versi %2
AdditionalIcons=Pintasan tambahan:
CreateDesktopIcon=Buat pintasan &desktop
CreateQuickLaunchIcon=Buat pintasan Pelontar &Cepat
ProgramOnTheWeb=%1 di Web
UninstallProgram=Lepas %1
LaunchProgram=Jalankan %1
AssocFileExtension=&Kaitkan %1 dengan ekstensi berkas %2
AssocingFileExtension=Mengaitkan %1 dengan ekstensi berkas %2...
AutoStartProgramGroupDescription=Startup:
AutoStartProgram=Otomatis jalankan %1
AddonHostProgramNotFound=%1 tidak dapat ditemukan di map yang Anda pilih.%n%nMaju terus?
+157
View File
@@ -0,0 +1,157 @@
; Script generated by the Inno Setup Script Wizard.
#define MyAppName "YTSage"
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#define MyAppPublisher "oop7"
#define MyAppURL "https://github.com/oop7/YTSage/"
#ifndef MyAppExeName
#define MyAppExeName "YTSage-ffmpeg.exe"
#endif
#ifndef SourceDir
#define SourceDir "..\dist\YTSage-FFmpeg"
#endif
[Setup]
AppId={{56997322-2A3A-4338-AEF1-C3C8BB28AC4F}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
LicenseFile=..\LICENSE
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputBaseFilename=YTSage-v{#MyAppVersion}-ffmpeg-Setup
SolidCompression=yes
WizardStyle=modern
OutputDir=..\artifacts
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "arabic"; MessagesFile: "compiler:Languages\Arabic.isl"
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
Name: "chinesesimplified"; MessagesFile: "Languages\Unofficial\ChineseSimplified.isl"
Name: "hindi"; MessagesFile: "Languages\Unofficial\Hindi.islu"
Name: "indonesian"; MessagesFile: "Languages\Unofficial\Indonesian.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
Name: "addtopath"; Description: "Add ffmpeg to user PATH environment variable"; GroupDescription: "Additional options:"; Flags: unchecked
[Files]
Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Registry]
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath('{app}')
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Code]
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
ParamExpanded: string;
begin
// Expand the setup constants like {app} from Param
ParamExpanded := ExpandConstant(Param);
if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then
begin
Result := True;
exit;
end;
// Look for the path with leading and trailing semicolon
Result := Pos(';' + UpperCase(ParamExpanded) + ';', ';' + UpperCase(OrigPath) + ';') = 0;
if Result = True then
Result := Pos(';' + UpperCase(ParamExpanded) + '\;', ';' + UpperCase(OrigPath) + ';') = 0;
end;
procedure UpdateLanguageConfig;
var
LanguageCode: string;
ConfigPath: string;
FileContent: AnsiString;
JsonContent: string;
LangPattern: string;
P_Start, P_End: Integer;
begin
if ActiveLanguage = 'english' then LanguageCode := 'en'
else if ActiveLanguage = 'arabic' then LanguageCode := 'ar'
else if ActiveLanguage = 'brazilianportuguese' then LanguageCode := 'pt-br'
else if ActiveLanguage = 'french' then LanguageCode := 'fr'
else if ActiveLanguage = 'german' then LanguageCode := 'de'
else if ActiveLanguage = 'italian' then LanguageCode := 'it'
else if ActiveLanguage = 'japanese' then LanguageCode := 'ja'
else if ActiveLanguage = 'polish' then LanguageCode := 'pl'
else if ActiveLanguage = 'portuguese' then LanguageCode := 'pt'
else if ActiveLanguage = 'russian' then LanguageCode := 'ru'
else if ActiveLanguage = 'spanish' then LanguageCode := 'es'
else if ActiveLanguage = 'turkish' then LanguageCode := 'tr'
else if ActiveLanguage = 'chinesesimplified' then LanguageCode := 'zh'
else if ActiveLanguage = 'hindi' then LanguageCode := 'hi'
else if ActiveLanguage = 'indonesian' then LanguageCode := 'id'
else LanguageCode := 'en';
ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json');
if not DirExists(ExtractFilePath(ConfigPath)) then
ForceDirectories(ExtractFilePath(ConfigPath));
if FileExists(ConfigPath) then
begin
if LoadStringFromFile(ConfigPath, FileContent) then
begin
JsonContent := String(FileContent);
LangPattern := '"language": "';
P_Start := Pos(LangPattern, JsonContent);
if P_Start > 0 then
begin
P_Start := P_Start + Length(LangPattern);
P_End := Pos('"', Copy(JsonContent, P_Start, Length(JsonContent)));
if P_End > 0 then
begin
Delete(JsonContent, P_Start, P_End - 1);
Insert(LanguageCode, JsonContent, P_Start);
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
end;
end
else
begin
JsonContent := '{' + #13#10 +
' "language": "' + LanguageCode + '"' + #13#10 +
'}';
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
UpdateLanguageConfig();
end;
end;
+135
View File
@@ -0,0 +1,135 @@
; Script generated by the Inno Setup Script Wizard.
#define MyAppName "YTSage"
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#define MyAppPublisher "oop7"
#define MyAppURL "https://github.com/oop7/YTSage/"
#ifndef MyAppExeName
#define MyAppExeName "YTSage.exe"
#endif
#ifndef SourceDir
#define SourceDir "..\dist\YTSage"
#endif
[Setup]
AppId={{AE618DBF-DD56-462D-9C09-2C2B7A41B201}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
LicenseFile=..\LICENSE
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputBaseFilename=YTSage-v{#MyAppVersion}-Setup
SolidCompression=yes
WizardStyle=modern
OutputDir=..\artifacts
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "arabic"; MessagesFile: "compiler:Languages\Arabic.isl"
Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl"
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl"
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl"
Name: "chinesesimplified"; MessagesFile: "Languages\Unofficial\ChineseSimplified.isl"
Name: "hindi"; MessagesFile: "Languages\Unofficial\Hindi.islu"
Name: "indonesian"; MessagesFile: "Languages\Unofficial\Indonesian.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Files]
Source: "{#SourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Code]
procedure UpdateLanguageConfig;
var
LanguageCode: string;
ConfigPath: string;
FileContent: AnsiString;
JsonContent: string;
LangPattern: string;
P_Start, P_End: Integer;
begin
if ActiveLanguage = 'english' then LanguageCode := 'en'
else if ActiveLanguage = 'arabic' then LanguageCode := 'ar'
else if ActiveLanguage = 'brazilianportuguese' then LanguageCode := 'pt-br'
else if ActiveLanguage = 'french' then LanguageCode := 'fr'
else if ActiveLanguage = 'german' then LanguageCode := 'de'
else if ActiveLanguage = 'italian' then LanguageCode := 'it'
else if ActiveLanguage = 'japanese' then LanguageCode := 'ja'
else if ActiveLanguage = 'polish' then LanguageCode := 'pl'
else if ActiveLanguage = 'portuguese' then LanguageCode := 'pt'
else if ActiveLanguage = 'russian' then LanguageCode := 'ru'
else if ActiveLanguage = 'spanish' then LanguageCode := 'es'
else if ActiveLanguage = 'turkish' then LanguageCode := 'tr'
else if ActiveLanguage = 'chinesesimplified' then LanguageCode := 'zh'
else if ActiveLanguage = 'hindi' then LanguageCode := 'hi'
else if ActiveLanguage = 'indonesian' then LanguageCode := 'id'
else LanguageCode := 'en';
ConfigPath := ExpandConstant('{localappdata}\YTSage\data\ytsage_config.json');
if not DirExists(ExtractFilePath(ConfigPath)) then
ForceDirectories(ExtractFilePath(ConfigPath));
if FileExists(ConfigPath) then
begin
if LoadStringFromFile(ConfigPath, FileContent) then
begin
JsonContent := String(FileContent);
LangPattern := '"language": "';
P_Start := Pos(LangPattern, JsonContent);
if P_Start > 0 then
begin
P_Start := P_Start + Length(LangPattern);
P_End := Pos('"', Copy(JsonContent, P_Start, Length(JsonContent)));
if P_End > 0 then
begin
Delete(JsonContent, P_Start, P_End - 1);
Insert(LanguageCode, JsonContent, P_Start);
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
end;
end
else
begin
JsonContent := '{' + #13#10 +
' "language": "' + LanguageCode + '"' + #13#10 +
'}';
SaveStringToFile(ConfigPath, AnsiString(JsonContent), False);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
UpdateLanguageConfig();
end;
end;
+8
View File
@@ -0,0 +1,8 @@
"""
YTSage - YouTube Video Downloader
A modern, user-friendly YouTube video downloader built with PySide6.
"""
__version__ = "5.2.0"
__author__ = "oop7"
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
"""
Core functionality modules for YTSage.
This package contains the core business logic and utility functions.
"""
+780
View File
@@ -0,0 +1,780 @@
import os
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from typing import Optional
import requests
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QMessageBox,
QProgressBar,
QPushButton,
QVBoxLayout,
)
from ..utils.ytsage_logger import logger
from ..utils.ytsage_localization import _
from ..utils.ytsage_constants import (
APP_BIN_DIR,
ICON_PATH,
OS_FULL_NAME,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
DENO_APP_BIN_PATH,
DENO_DOWNLOAD_URL,
DENO_SHA256_URL,
)
from .ytsage_ffmpeg import get_file_sha256
def verify_deno_sha256(file_path: Path, sha256_url: str) -> bool:
"""
Verify Deno file SHA256 hash against official checksums.
Args:
file_path: Path to the downloaded Deno zip file
sha256_url: URL to download the SHA256 checksum file
Returns:
bool: True if verification successful, False otherwise
"""
try:
# Download the SHA256 checksum file
logger.info(f"Downloading SHA256 checksum from: {sha256_url}")
response = requests.get(sha256_url, timeout=10)
response.raise_for_status()
checksum_content = response.text
# Parse the checksum file
# Format can be either:
# 1. Standard Unix format: "hash filename"
# 2. Verbose format: "Hash : <HASH>"
expected_hash = None
for line in checksum_content.strip().split("\n"):
line = line.strip()
if not line:
continue
# Try standard Unix format first (hash followed by spaces and filename)
if len(line) >= 64 and (" " in line or "\t" in line):
# Extract first 64 characters as potential hash
potential_hash = line.split()[0]
if len(potential_hash) == 64 and all(c in "0123456789abcdefABCDEF" for c in potential_hash):
expected_hash = potential_hash
break
# Try verbose format
if line.startswith("Hash"):
parts = line.split(":", 1)
if len(parts) == 2:
expected_hash = parts[1].strip()
break
if not expected_hash:
logger.error("Could not find SHA256 hash in checksum file")
logger.debug(f"Checksum file content: {checksum_content}")
return False
# Calculate actual hash of downloaded file
logger.info("Calculating SHA256 hash of downloaded file...")
actual_hash = get_file_sha256(file_path)
# Compare hashes (case-insensitive)
if actual_hash.lower() == expected_hash.lower():
logger.info("✓ SHA256 verification successful!")
logger.info(f" Expected: {expected_hash}")
logger.info(f" Actual: {actual_hash}")
return True
else:
logger.error("✗ SHA256 verification failed!")
logger.error(f" Expected: {expected_hash}")
logger.error(f" Actual: {actual_hash}")
return False
except requests.RequestException as e:
logger.error(f"Failed to download SHA256 checksum: {e}")
return False
except Exception as e:
logger.exception(f"Error during SHA256 verification: {e}")
return False
class DownloadDenoThread(QThread):
progress_signal = Signal(int)
status_signal = Signal(str)
finished_signal = Signal(bool, str)
def __init__(self):
super().__init__()
def run(self) -> None:
temp_zip_path = None
try:
# Create temporary file for zip download
temp_zip_fd, temp_zip_path = tempfile.mkstemp(suffix=".zip")
os.close(temp_zip_fd) # Close the file descriptor
# Download with progress reporting
logger.info(f"Downloading Deno from: {DENO_DOWNLOAD_URL}")
self.status_signal.emit(_("deno.downloading"))
response = requests.get(DENO_DOWNLOAD_URL, stream=True)
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
block_size = 8192 # 8KB blocks
if total_size == 0:
self.progress_signal.emit(100)
with open(temp_zip_path, "wb") as f:
downloaded = 0
for data in response.iter_content(block_size):
f.write(data)
downloaded += len(data)
if total_size > 0:
progress = int(downloaded / total_size * 100)
self.progress_signal.emit(progress)
logger.info("Download complete, verifying SHA256 hash...")
self.status_signal.emit(_("deno.verifying"))
# Verify SHA256 hash
if not verify_deno_sha256(Path(temp_zip_path), DENO_SHA256_URL):
# Hash verification failed - delete the downloaded file
logger.error("SHA256 verification failed! Removing downloaded file.")
if Path(temp_zip_path).exists():
Path(temp_zip_path).unlink()
self.finished_signal.emit(
False,
_("deno.verification_failed")
)
return
# Extract deno executable from zip
logger.info("Extracting Deno executable...")
self.status_signal.emit(_("deno.extracting"))
with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref:
# Deno zip contains just the executable at root
executable_name = "deno.exe" if OS_NAME == "Windows" else "deno"
# Find the executable in the zip
if executable_name not in zip_ref.namelist():
logger.error(f"Executable '{executable_name}' not found in zip file")
self.finished_signal.emit(False, f"Executable '{executable_name}' not found in zip")
return
# 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
if not exe_path.exists():
logger.error(f"Extraction failed: {exe_path} does not exist")
self.finished_signal.emit(False, "Extraction failed")
return
# Make executable on macOS and Linux
if OS_NAME != "Windows":
os.chmod(exe_path, 0o755)
logger.info("Set executable permissions on Unix system")
# Clean up the temporary zip file
if temp_zip_path and Path(temp_zip_path).exists():
Path(temp_zip_path).unlink()
logger.info("Cleaned up temporary zip file")
logger.info("Deno downloaded, verified, and extracted successfully!")
self.finished_signal.emit(True, str(exe_path))
except Exception as e:
logger.exception(f"Error downloading/extracting Deno: {e}")
# Clean up temporary zip file on error
if temp_zip_path and Path(temp_zip_path).exists():
try:
Path(temp_zip_path).unlink()
except Exception:
pass
self.finished_signal.emit(False, str(e))
class DenoSetupDialog(QDialog):
setup_complete = Signal(str) # Signal emitting the path to Deno
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle(_("deno.setup_required"))
self.setMinimumWidth(520)
self.setMinimumHeight(300)
self.resize(520, 320)
# Set the window icon to match the main app
if parent and parent.windowIcon():
self.setWindowIcon(parent.windowIcon())
else:
icon_path = ICON_PATH
if Path.exists(icon_path):
self.setWindowIcon(QIcon(str(icon_path)))
self.init_ui()
# Apply dark theme styling to match app
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #cccccc;
line-height: 1.4;
}
QPushButton {
padding: 10px 20px;
background-color: #c90000;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-width: 120px;
min-height: 20px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
QPushButton:disabled {
background-color: #666666;
color: #999999;
}
QProgressBar {
border: 2px solid #1d1e22;
border-radius: 6px;
text-align: center;
color: white;
background-color: #1d1e22;
height: 25px;
font-weight: bold;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #e60000, stop: 0.5 #ff3333, stop: 1 #c90000);
border-radius: 4px;
margin: 1px;
}
"""
)
def init_ui(self) -> None:
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(25, 25, 25, 25)
# Header title
title_label = QLabel(_("deno.setup_required"))
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
# Information label with improved styling
info_label = QLabel(
_("deno.setup_description", os_name=OS_FULL_NAME)
)
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
info_label.setWordWrap(True)
info_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 5px; line-height: 1.4;")
layout.addWidget(info_label)
# Progress bar with proper sizing
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setFixedHeight(20)
self.progress_bar.setStyleSheet(
"""
QProgressBar {
border: 1px solid #3d3d3d;
border-radius: 8px;
background-color: #1d1e22;
text-align: center;
color: #ffffff;
font-size: 12px;
font-weight: bold;
height: 20px;
}
QProgressBar::chunk {
background-color: #c90000;
border-radius: 6px;
margin: 1px;
}
"""
)
layout.addWidget(self.progress_bar)
# Status label with better spacing
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("font-size: 12px; color: #cccccc; padding: 8px 0;")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
# Add stretch to push buttons to bottom
layout.addStretch()
# Button layout with improved spacing
button_layout = QHBoxLayout()
button_layout.setSpacing(15)
button_layout.setContentsMargins(0, 10, 0, 0)
self.setup_button = QPushButton(_("deno.setup_button"))
self.setup_button.clicked.connect(self.download_deno)
self.cancel_button = QPushButton(_("buttons.cancel"))
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.setup_button)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def download_deno(self) -> None:
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText(_("deno.downloading"))
self.setup_button.setEnabled(False)
self.cancel_button.setEnabled(False)
self.download_thread = DownloadDenoThread()
self.download_thread.progress_signal.connect(self.update_progress)
self.download_thread.status_signal.connect(self.update_status)
self.download_thread.finished_signal.connect(self.download_finished)
self.download_thread.start()
def update_progress(self, value) -> None:
self.progress_bar.setValue(value)
def update_status(self, status: str) -> None:
self.status_label.setText(status)
def download_finished(self, success, result) -> None:
self.setup_button.setEnabled(True)
self.cancel_button.setEnabled(True)
if success:
self.status_label.setText(_("deno.success"))
self.setup_complete.emit(result)
self.accept()
else:
self.status_label.setText(f"{_('deno.download_error', error=result)}")
error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle(_("deno.download_failed"))
error_dialog.setText(_("deno.download_error", error=result))
error_dialog.setWindowIcon(self.windowIcon())
error_dialog.setStyleSheet(
"""
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;
}
"""
)
error_dialog.exec()
def check_deno_binary() -> Optional[Path]:
"""
Check if Deno binary exists in the app's bin directory ONLY.
We only use our managed binary, not system PATH.
Returns:
Path or None: Path to Deno binary if found in app bin, None otherwise
"""
exe_path = DENO_APP_BIN_PATH
if exe_path.exists():
# Make sure it's executable on Unix systems
if OS_NAME != "Windows" and not os.access(exe_path, os.X_OK):
try:
os.chmod(exe_path, 0o755)
logger.info(f"Fixed permissions on Deno at {exe_path}")
except Exception as e:
logger.exception(f"Could not set executable permissions on {exe_path}: {e}")
logger.info(f"Found Deno in app bin directory: {exe_path}")
return exe_path
# Binary not found in app directory - return None to trigger setup
logger.warning(f"Deno binary not found in app bin directory: {exe_path}")
return None
def check_deno_installed() -> bool:
"""
Check if Deno is installed and accessible.
Returns:
bool: True if Deno is found and working, False otherwise
"""
try:
deno_path = check_deno_binary()
if deno_path:
# Try to run deno --version to verify it's working
try:
result = subprocess.run(
[str(deno_path), "--version"],
capture_output=True,
text=True,
timeout=5,
creationflags=SUBPROCESS_CREATIONFLAGS
)
return result.returncode == 0
except Exception:
return False
return False
except Exception:
return False
def get_deno_path() -> Path:
"""
Get the Deno path from the app's bin directory.
Returns:
Path or str: Path to Deno binary, or "deno" as fallback command
"""
deno_path = check_deno_binary()
if deno_path:
logger.info(f"Using Deno from: {deno_path}")
return deno_path
# If not found, fall back to the command name as a last resort
logger.info("Deno not found in app directory, falling back to command name")
return "deno" # type: ignore[return-value]
def get_deno_version_direct(deno_path=None) -> str:
"""
Get Deno version directly without caching.
Args:
deno_path: Optional path to Deno binary. If None, uses get_deno_path()
Returns:
str: Version string or error message
"""
try:
if deno_path is None:
deno_path = get_deno_path()
if not deno_path or deno_path == "deno":
return "Not found"
result = subprocess.run(
[str(deno_path), "--version"],
capture_output=True,
text=True,
timeout=10,
creationflags=SUBPROCESS_CREATIONFLAGS
)
if result.returncode == 0:
# Deno outputs: "deno 1.38.0 (release, x86_64-pc-windows-msvc)"
# Extract version from first line
lines = result.stdout.strip().split("\n")
if lines:
first_line = lines[0]
# Extract version number (e.g., "1.38.0" from "deno 1.38.0 ...")
parts = first_line.split()
if len(parts) >= 2 and parts[0] == "deno":
return parts[1]
return first_line.strip()
return "Unknown version"
else:
return "Error getting version"
except Exception as e:
logger.exception(f"Error getting Deno version: {e}")
return "Error getting version"
def setup_deno(parent_widget=None):
"""
Show the Deno setup dialog and handle the result.
Returns:
str: Path to Deno binary
"""
logger.debug("Starting Deno setup dialog")
dialog = DenoSetupDialog(parent_widget)
# Store the setup result from the signal
setup_result = {"path": None}
def on_setup_complete(path) -> None:
logger.debug(f"Received setup_complete signal with path: {path}")
setup_result["path"] = path
# Connect to the setup_complete signal
dialog.setup_complete.connect(on_setup_complete)
# Show the dialog
result = dialog.exec()
logger.debug(f"Dialog result: {result} (Accepted={QDialog.DialogCode.Accepted})")
if result == QDialog.DialogCode.Accepted:
# First check if we received a path from the signal
if setup_result["path"]:
path_obj = Path(setup_result["path"]) if isinstance(setup_result["path"], str) else setup_result["path"]
if path_obj.exists():
logger.debug(f"Using path from signal: {setup_result['path']}")
return str(setup_result["path"])
# Get the expected path for verification as fallback
expected_path = DENO_APP_BIN_PATH
logger.debug(f"Expected Deno path: {expected_path}")
# Verify the path exists after dialog is accepted
if expected_path.exists():
logger.debug(f"Deno successfully found at expected path: {expected_path}")
return str(expected_path)
else:
logger.debug(f"Expected path does not exist, trying alternate detection")
# Try to use the get_deno_path function to find Deno elsewhere
deno_path = get_deno_path()
logger.debug(f"Alternate detection result: {deno_path}")
if deno_path != "deno":
path_obj = Path(deno_path) if isinstance(deno_path, str) else deno_path
if path_obj.exists():
logger.debug(f"Deno found at alternate location: {deno_path}")
return str(deno_path)
# Something went wrong, show an error message
logger.debug(f"Setup failed, showing error dialog")
if parent_widget:
error_dialog = QMessageBox(parent_widget)
error_dialog.setIcon(QMessageBox.Icon.Warning)
error_dialog.setWindowTitle(_("deno.setup_error"))
error_dialog.setText(_("deno.setup_failed"))
error_dialog.setWindowIcon(parent_widget.windowIcon())
error_dialog.setStyleSheet(
"""
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;
}
"""
)
error_dialog.exec()
logger.warning(f"Deno setup failed, path does not exist: {expected_path}")
else:
logger.debug("User cancelled the setup dialog")
# User cancelled or setup failed, return the fallback command
logger.debug("Returning fallback command 'deno'")
return "deno"
def get_latest_deno_version() -> Optional[str]:
"""
Fetch the latest Deno version from GitHub API.
Returns:
str: Version string (e.g., "2.5.6") or None if fetch failed
"""
try:
response = requests.get(
"https://api.github.com/repos/denoland/deno/releases/latest",
timeout=10
)
response.raise_for_status()
data = response.json()
# Get tag_name (e.g., "v2.5.6") and remove 'v' prefix
tag_name = data.get("tag_name", "")
if tag_name.startswith("v"):
version = tag_name[1:]
else:
version = tag_name
logger.info(f"Latest Deno version: {version}")
return version
except requests.RequestException as e:
logger.error(f"Failed to fetch latest Deno version: {e}")
return None
except Exception as e:
logger.exception(f"Unexpected error fetching Deno version: {e}")
return None
def compare_deno_versions(current: str, latest: str) -> bool:
"""
Compare two Deno version strings.
Args:
current: Current version string (e.g., "2.5.6")
latest: Latest version string (e.g., "2.5.7")
Returns:
bool: True if update is needed (latest > current), False otherwise
"""
try:
import re
def parse_version(version_str: str) -> tuple:
"""Parse version string into tuple of integers."""
# Remove 'v' prefix if present
if version_str.startswith('v'):
version_str = version_str[1:]
# Extract version numbers
match = re.search(r'(\d+\.\d+\.\d+)', version_str)
if match:
version_str = match.group(1)
parts = version_str.split('.')
return tuple(int(p) for p in parts if p.isdigit())
current_tuple = parse_version(current)
latest_tuple = parse_version(latest)
logger.debug(f"Comparing Deno versions: {current_tuple} vs {latest_tuple}")
return latest_tuple > current_tuple
except (ValueError, AttributeError) as e:
logger.warning(f"Could not compare Deno versions: {e}")
return False
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
"""
try:
deno_path = DENO_APP_BIN_PATH
if not deno_path.exists():
error_msg = f"Deno binary not found at: {deno_path}"
logger.error(error_msg)
return False, error_msg
logger.info(f"Upgrading Deno using: {deno_path}")
# Run deno upgrade command with output capturing
process = subprocess.Popen(
[str(deno_path), "upgrade"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
creationflags=SUBPROCESS_CREATIONFLAGS,
bufsize=1, # Line buffered
encoding='utf-8',
errors='replace'
)
full_output = []
# 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")
return True, output
else:
logger.error(f"Deno upgrade failed with code {return_code}")
logger.error(f"Output: {output}")
return False, output
except subprocess.TimeoutExpired:
error_msg = "Deno upgrade timed out after 5 minutes"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Error upgrading Deno: {str(e)}"
logger.exception(error_msg)
return False, error_msg
def check_deno_update() -> tuple[bool, str, str]:
"""
Check if a Deno update is available.
Returns:
tuple: (update_needed: bool, current_version: str, latest_version: str)
"""
try:
# Get current version
current_version = get_deno_version_direct()
if current_version in ["Not found", "Error getting version"]:
return False, current_version, "Unknown"
# Get latest version
latest_version = get_latest_deno_version()
if not latest_version:
return False, current_version, "Error"
# Compare versions
update_needed = compare_deno_versions(current_version, latest_version)
return update_needed, current_version, latest_version
except Exception as e:
logger.exception(f"Error checking Deno update: {e}")
return False, "Error", "Error"
+838
View File
@@ -0,0 +1,838 @@
import gc
import os
import re
import shlex # For safely parsing command arguments
import signal
import subprocess # For direct CLI command execution
import sys
import time
from pathlib import Path
from typing import Optional, List, Set
from PySide6.QtCore import QObject, QThread, Signal
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
class SignalManager(QObject):
update_formats = Signal(list)
update_status = Signal(str)
update_progress = Signal(float)
playlist_info_label_visible = Signal(bool)
playlist_info_label_text = Signal(str)
selected_subs_label_text = Signal(str)
playlist_select_btn_visible = Signal(bool)
playlist_select_btn_text = Signal(str)
class DownloadThread(QThread):
progress_signal = Signal(float)
status_signal = Signal(str)
finished_signal = Signal()
error_signal = Signal(str)
file_exists_signal = Signal(str) # New signal for file existence
update_details = Signal(str) # New signal for filename, speed, ETA
def __init__(
self,
url,
path,
format_id,
is_audio_only=False,
format_has_audio=False,
subtitle_langs=None,
is_playlist=False,
merge_subs=False,
enable_sponsorblock=False,
sponsorblock_categories=None,
resolution="",
playlist_items=None,
save_description=False,
embed_chapters=False,
cookie_file=None,
browser_cookies=None,
rate_limit=None,
download_section=None,
force_keyframes=False,
proxy_url=None,
geo_proxy_url=None,
force_output_format=False,
preferred_output_format="mp4",
force_audio_format=False,
preferred_audio_format="best",
audio_normalization=False,
filename_format=None,
concurrent_fragments=1,
) -> None:
super().__init__()
self.url = url
self.path = Path(path)
self.format_id = format_id
self.is_audio_only = is_audio_only
self.format_has_audio = format_has_audio
self.subtitle_langs = subtitle_langs if subtitle_langs else []
self.is_playlist = is_playlist
self.merge_subs = merge_subs
self.enable_sponsorblock = enable_sponsorblock
self.sponsorblock_categories = sponsorblock_categories if sponsorblock_categories else ["sponsor"]
self.resolution = resolution
self.playlist_items = playlist_items
self.save_description = save_description
self.embed_chapters = embed_chapters
self.cookie_file = cookie_file
self.browser_cookies = browser_cookies
self.rate_limit = rate_limit
self.download_section = download_section
self.force_keyframes = force_keyframes
self.proxy_url = proxy_url
self.geo_proxy_url = geo_proxy_url
self.force_output_format = force_output_format
self.preferred_output_format = preferred_output_format
self.force_audio_format = force_audio_format
self.preferred_audio_format = preferred_audio_format
self.audio_normalization = audio_normalization
self.filename_format = filename_format
self.concurrent_fragments = concurrent_fragments
self.paused: bool = False
self.cancelled: bool = False
self.process: Optional[subprocess.Popen] = None
self.current_filename: Optional[str] = None # Initialize filename storage
self.last_file_path: Optional[str] = None # Initialize full file path storage
self.subtitle_files: List[str] = [] # Track subtitle files that are created
self.initial_subtitle_files: Set[Path] = set() # Track initial subtitle files before download
def cleanup_partial_files(self) -> None:
"""Delete any partial files including .part and unmerged format-specific files"""
try:
pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243.
for file_path in self.path.iterdir():
if file_path.suffix == ".part" or pattern.search(file_path.name):
self._safe_delete_with_retry(file_path)
except Exception as e:
logger.exception(f"Error cleaning partial files: {e}")
# Don't emit error signal for cleanup issues to avoid crashing the thread
logger.error(f"Error cleaning partial files: {e}")
def _safe_delete_with_retry(self, file_path: Path, max_retries: int = 5, delay: float = 2.0) -> None:
"""Safely delete a file with retry mechanism for file locking issues across platforms"""
for attempt in range(max_retries):
try:
# Force garbage collection to release any Python-held file handles
gc.collect()
if file_path.exists():
file_path.unlink(missing_ok=True)
logger.info(f"Successfully deleted {file_path.name}")
return
except PermissionError as e:
if attempt < max_retries - 1:
logger.warning(f"File {file_path.name} is locked, retrying in {delay} seconds... (attempt {attempt + 1}/{max_retries})")
time.sleep(delay)
delay = min(delay * 1.5, 5.0) # Exponential backoff, capped at 5 seconds
else:
logger.error(f"Failed to delete {file_path.name} after {max_retries} attempts: {e}")
return
except Exception as e:
logger.error(f"Error deleting {file_path.name}: {e}")
return
def _terminate_process_tree(self, process: subprocess.Popen) -> None:
"""Terminate a process and all its children across platforms"""
pid = process.pid
try:
if sys.platform == "win32":
# Windows: Use taskkill to kill the entire process tree
# /T = kill child processes, /F = force kill
# Use subprocess.run with no encoding to avoid codec issues
subprocess.run(
["taskkill", "/F", "/T", "/PID", str(pid)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=SUBPROCESS_CREATIONFLAGS,
)
logger.debug(f"Killed process tree on Windows (PID: {pid})")
else:
# Unix-like systems: Kill the process group
try:
# Try to kill the process group
os.killpg(os.getpgid(pid), signal.SIGTERM)
time.sleep(0.5)
# Force kill if still running
os.killpg(os.getpgid(pid), signal.SIGKILL)
except (ProcessLookupError, PermissionError):
# Process already terminated or no permission
pass
logger.debug(f"Killed process group on Unix (PID: {pid})")
except Exception as e:
logger.warning(f"Error killing process tree: {e}")
# Fallback to standard termination
try:
process.terminate()
process.wait(timeout=2)
except Exception:
try:
process.kill()
process.wait()
except Exception:
pass
# Ensure process is waited on to avoid zombies
try:
process.wait(timeout=3)
except Exception:
pass
def cleanup_subtitle_files(self) -> None:
"""Delete subtitle files after they have been merged into the video file"""
deleted_count: List[int] = [0, 0]
def safe_delete(path: Path) -> bool:
try:
# 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
try:
# --- Method 1: Delete tracked subtitle files ---
for f in self.subtitle_files or []:
deleted_count[0] += safe_delete(path=Path(f))
else:
logger.debug(f"Deleted {deleted_count[0]} of {len(self.subtitle_files)} tracked subtitle files")
# --- Method 2: Delete new subtitle files not in initial set ---
new_subtitle_files: Set[Path] = {
f for f in Path(self.path).rglob("*") if f.suffix in [".vtt", ".srt"] and f not in self.initial_subtitle_files
}
for subtitle_file in new_subtitle_files:
deleted_count[1] += safe_delete(path=subtitle_file)
else:
logger.debug(f"Deleted {deleted_count[1]} of {len(new_subtitle_files)} new subtitle files")
except Exception as e:
logger.exception(f"Error cleaning subtitle files: {e}")
def _build_yt_dlp_command(self) -> List[str]:
"""Build the yt-dlp command line with all options for direct execution."""
yt_dlp_path: str = get_yt_dlp_path()
# Build the command line array
cmd: List[str] = [yt_dlp_path]
logger.debug(f"Using yt-dlp from: {yt_dlp_path}")
# Add concurrent fragments setting
if self.concurrent_fragments:
cmd.extend(["-N", str(self.concurrent_fragments)])
logger.debug(f"Using {self.concurrent_fragments} concurrent connections")
# Format selection strategy - use format ID if provided or fallback to resolution
if self.is_playlist:
# For playlists, specific format_id from the first video often fails for subsequent videos.
# Instead, we rely on dynamic fallback/resolution limits.
if self.is_audio_only:
# For audio-only playlist, let yt-dlp pick best audio.
cmd.extend(["-f", "bestaudio/best"])
logger.debug(f"Playlist mode: using dynamic best audio fallback instead of format_id")
else:
# If a specific resolution is given, limit to it. Otherwise, select the overall best.
# The resolution might be e.g. "1920x1080" or "1080". We want the height.
try:
if self.resolution and self.resolution != "default":
res_str = str(self.resolution)
h = min(map(int, res_str.split('x'))) if 'x' in res_str else int(res_str)
cmd.extend(["-S", f"res:{h}"])
logger.debug(f"Playlist mode: using resolution limiter -S res:{h}")
else:
cmd.extend(["-f", "bestvideo+bestaudio/best"])
logger.debug("Playlist mode: using dynamic best quality overall")
except ValueError:
cmd.extend(["-f", "bestvideo+bestaudio/best"])
logger.debug("Playlist mode: invalid resolution string, using dynamic best quality overall")
elif self.format_id:
clean_format_id: str = self.format_id.split("-drc")[0] if "-drc" in self.format_id else self.format_id
# If the selected format is audio-only, pass it directly.
if self.is_audio_only:
cmd.extend(["-f", clean_format_id])
logger.debug(f"Using audio-only format selection: {clean_format_id}")
# If the selected format already includes an audio track (progressive), no merge needed.
elif self.format_has_audio:
cmd.extend(["-f", clean_format_id])
logger.debug(f"Using progressive format with bundled audio: {clean_format_id}")
else:
cmd.extend(["-f", f"{clean_format_id}+bestaudio/best"])
logger.debug(f"Using video-only format merged with best audio: {clean_format_id}+bestaudio/best")
else:
# If no specific format ID, use resolution-based sorting (-S)
res_value: str = self.resolution if self.resolution else "720" # Default to 720p if no resolution specified
cmd.extend(["-S", f"res:{res_value}"])
# Force output format if enabled and merging is needed (for video)
if self.force_output_format and not self.is_audio_only:
if self.format_has_audio:
# Progressive format (video with audio) - use remux to convert container
cmd.extend(["--remux-video", self.preferred_output_format])
logger.debug(f"Using --remux-video to force progressive format to: {self.preferred_output_format}")
else:
# Merging video+audio - force merge output format
cmd.extend(["--merge-output-format", self.preferred_output_format])
logger.debug(f"Using --merge-output-format to force merged format to: {self.preferred_output_format}")
# Force audio format conversion for audio-only downloads
if self.is_audio_only and self.force_audio_format:
cmd.append("--extract-audio")
if self.preferred_audio_format and self.preferred_audio_format != "best":
cmd.extend(["--audio-format", self.preferred_audio_format])
logger.debug(f"Using --extract-audio with --audio-format {self.preferred_audio_format} for audio-only download")
else:
logger.debug("Using --extract-audio with best quality (no conversion) for audio-only download")
# Add Audio Normalization if enabled (only applies to audio-only downloads)
if self.audio_normalization and self.is_audio_only:
# Normalization using FFmpeg filters requires re-encoding the audio stream.
# If the user selected "Best (No conversion)", yt-dlp attempts to stream copy (-c:a copy),
# which will cause FFmpeg to crash with "Invalid argument".
# We fix this by forcing an explicit actual conversion (mp3) if no format was forced.
if not self.force_audio_format or self.preferred_audio_format == "best":
if "--extract-audio" not in cmd:
cmd.append("--extract-audio")
cmd.extend(["--audio-format", "mp3"])
logger.debug("Forced audio format to mp3 since normalization requires re-encoding")
# Scope the argument specifically to ExtractAudio so it doesn't conflict with other PPs
cmd.extend(["--postprocessor-args", "ExtractAudio:-af loudnorm=I=-16:LRA=11:TP=-1.5"])
logger.debug("Added Audio Normalization (--postprocessor-args ExtractAudio:-af loudnorm=...)")
# Output template with resolution in filename
# 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_[%(id)s].%(ext)s"
if self.is_playlist:
# Create output template with playlist subfolder
output_template: str = f"{base_path}/%(playlist_title)s/{filename_part}"
else:
# For single files, automatically ignore/remove playlist-specific preamble (like "%(playlist_index)s - ")
import re
filename_part = re.sub(r'%\(playlist_index[^)]*\)[a-zA-Z0-9]*\s*(?:[-_]\s*)?', '', filename_part)
output_template: str = f"{base_path}/{filename_part}"
cmd.extend(["-o", str(output_template)])
# Add common options
cmd.append("--force-overwrites")
# Add playlist items if specified
if self.is_playlist and self.playlist_items:
cmd.extend(["--playlist-items", self.playlist_items])
# Add subtitle options if subtitles are selected
if self.subtitle_langs:
# Subtitles work with both audio-only and video formats
# For audio-only formats, subtitles will be downloaded as separate files
cmd.append("--write-subs")
# Get language codes from subtitle selections
lang_codes: List[str] = []
has_auto_generated = False
for sub_selection in self.subtitle_langs:
try:
# Extract just the language code (e.g., 'en' from 'en - Manual')
lang_code = sub_selection.split(" - ")[0]
lang_codes.append(lang_code)
if "Auto-generated" in sub_selection:
has_auto_generated = True
except Exception as e:
logger.exception(f"Could not parse subtitle selection '{sub_selection}': {e}")
if lang_codes:
cmd.extend(["--sub-langs", ",".join(lang_codes)])
if has_auto_generated:
cmd.append("--write-auto-subs") # Include auto-generated subtitles
# Only embed subtitles if merge is enabled
if self.merge_subs:
cmd.append("--embed-subs")
# Add SponsorBlock if enabled
if self.enable_sponsorblock and self.sponsorblock_categories:
cmd.append("--sponsorblock-remove")
cmd.append(",".join(self.sponsorblock_categories))
# Add description saving if enabled
if self.save_description:
cmd.append("--write-description")
# Add chapters embedding if enabled
if self.embed_chapters:
cmd.append("--embed-chapters")
# Add cookies if specified
if self.cookie_file:
cmd.extend(["--cookies", str(self.cookie_file)])
elif self.browser_cookies:
cmd.extend(["--cookies-from-browser", self.browser_cookies])
# Add proxy settings if specified
if self.proxy_url:
cmd.extend(["--proxy", self.proxy_url])
if self.geo_proxy_url:
cmd.extend(["--geo-verification-proxy", self.geo_proxy_url])
# Add rate limit if specified
if self.rate_limit:
cmd.extend(["-r", self.rate_limit])
# Add download section if specified
if self.download_section:
cmd.extend(["--download-sections", self.download_section])
# Add force keyframes option if enabled
if self.force_keyframes:
cmd.append("--force-keyframes-at-cuts")
logger.debug(f"Added download section: {self.download_section}, Force keyframes: {self.force_keyframes}")
# Add the URL as the final argument
if self.is_playlist:
cmd.append("--ignore-errors")
cmd.append("--no-abort-on-error")
cmd.append(self.url)
return cmd
def run(self) -> None:
try:
logger.debug("Starting download thread")
# Get initial list of subtitle files to compare later
self.initial_subtitle_files = set()
if self.merge_subs:
try:
# Scan for existing subtitle files in the directory
for file in self.path.rglob("*"):
if file.suffix in {".vtt", ".srt"}:
self.initial_subtitle_files.add(file)
logger.debug(f"Found {len(self.initial_subtitle_files)} existing subtitle files before download")
except Exception as e:
logger.exception(f"Error scanning for initial subtitle files: {e}")
# Use direct CLI command
self._run_direct_command()
except Exception as e:
# Catch errors during setup
logger.critical(f"Critical error in download thread: {e}", exc_info=True)
self.error_signal.emit(f"Critical error in download thread: {e}")
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}")
self.status_signal.emit(_("download.starting"))
self.progress_signal.emit(0)
# Start the process
# Extra logic moved to src\utils\ytsage_constants.py
# Use start_new_session on Unix to enable process group termination
popen_kwargs = {
"stdout": subprocess.PIPE,
"stderr": subprocess.STDOUT,
"bufsize": 1, # Line buffered
"encoding": "utf-8",
"errors": "replace",
}
if sys.platform == "win32":
popen_kwargs["creationflags"] = SUBPROCESS_CREATIONFLAGS
else:
# On Unix, start a new session so we can kill the entire process group
popen_kwargs["start_new_session"] = True
self.process = subprocess.Popen(cmd, **popen_kwargs)
# Process output line by line to update progress
for line in iter(self.process.stdout.readline, ""): # type: ignore
if self.cancelled:
# Kill the entire process tree (yt-dlp + ffmpeg children)
self._terminate_process_tree(self.process)
# Add delay before cleanup to allow file handles to be released
time.sleep(2)
self.cleanup_partial_files()
self.status_signal.emit(_("download.cancelled"))
self.finished_signal.emit()
return
# Wait if paused
while self.paused and not self.cancelled:
time.sleep(0.1)
# Parse the line for download progress and status updates
self._parse_output_line(line)
# Wait for process to complete
return_code: int = self.process.wait()
# Special handling for specific errors
# return code 127 typically means command not found
if return_code == 127:
self.error_signal.emit(
_("errors.ytdlp_not_found_path")
)
return
if return_code == 0 or (self.is_playlist and return_code != 0 and self.current_filename is not None):
self.progress_signal.emit(100)
# Robust file finding: Always search for the most recent file
# This handles all post-processing scenarios (merging, remuxing, subtitle embedding, etc.)
final_file_found = False
try:
# First, check if last_file_path exists and is valid
if self.last_file_path:
last_path = Path(self.last_file_path)
if last_path.exists() and last_path.is_file():
# File exists at the tracked path
self.current_filename = last_path.name
final_file_found = True
logger.info(f"Found file at tracked path: {self.last_file_path}")
# If not found at tracked path, search for the most recent file
if not final_file_found:
logger.info("Searching for most recent downloaded file...")
potential_files = []
# Search in download directory and subdirectories (for playlists)
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}'))
if potential_files:
# Sort by modification time and get the most recent
most_recent = max(potential_files, key=lambda p: p.stat().st_mtime)
# Verify it was modified recently (within last 30 seconds to account for post-processing)
time_since_modification = time.time() - most_recent.stat().st_mtime
if time_since_modification < 30:
self.last_file_path = str(most_recent)
self.current_filename = most_recent.name
final_file_found = True
logger.info(f"Found most recent file (modified {time_since_modification:.1f}s ago): {self.last_file_path}")
else:
logger.warning(f"Most recent file is too old ({time_since_modification:.1f}s), might not be the right one")
else:
logger.warning("No video/audio files found in download directory")
except Exception as e:
logger.error(f"Error finding final file: {e}", exc_info=True)
# Set completion status
if return_code != 0:
self.status_signal.emit(_("download.completed") + " (with some errors)")
else:
self.status_signal.emit(_("download.completed"))
# Clean up subtitle files if they were merged, with a small delay
# to ensure the embedding process has completed
if self.merge_subs:
# Add a significant delay to ensure ffmpeg has released all file handles
# and any post-processing is complete
self.status_signal.emit(_("download.completed_cleaning"))
time.sleep(3) # Increased delay to 3 seconds
self.cleanup_subtitle_files()
self.finished_signal.emit()
else:
# Check if it was cancelled
if self.cancelled:
self.status_signal.emit(_("download.cancelled"))
self.finished_signal.emit()
else:
# 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(
_("errors.ytdlp_failed", error=error_msg)
)
else:
# 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)
self.cleanup_partial_files()
except Exception as e:
logger.exception(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()
def _parse_output_line(self, line: str) -> None:
"""Parse yt-dlp command output to update progress and status."""
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)
if dest_match:
try:
filepath = dest_match.group(1).strip()
self.current_filename = Path(filepath).name
self.last_file_path = filepath # Store the full path for later cleanup
logger.debug(f"Extracted filename: {self.current_filename}") # DEBUG
# Check if this is an audio-only download by looking in the previous lines
is_audio_download = False
# Look for audio format indicators in the current line or preceding output
# yt-dlp typically mentions format like "Downloading format 251 - audio only"
if " - audio only" in line:
is_audio_download = True
# Check if the format ID is mentioned earlier in the line
format_match = re.search(r"Downloading format (\d+)", line)
if format_match:
format_id = format_match.group(1)
logger.debug(f"Detected format ID: {format_id}")
# Format IDs for audio typically have different patterns
# (like 140, 251 for audio vs 137, 248 for video)
# This is just a heuristic since format IDs can vary
# Determine file type based on extension and context
ext = Path(self.current_filename).suffix.lower()
# Check if this is explicitly an audio stream download
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 VIDEO_EXTENSIONS:
self.status_signal.emit(_("download.downloading_video"))
# Audio file extensions
elif ext in AUDIO_EXTENSIONS:
self.status_signal.emit(_("download.downloading_audio"))
# Subtitle file extensions
elif ext in SUBTITLE_EXTENSIONS:
self.status_signal.emit(_("download.downloading_subtitle"))
# Default case
else:
self.status_signal.emit(_("download.downloading"))
except Exception as e:
logger.exception(f"Error extracting filename from line '{line}': {e}")
self.status_signal.emit(_("download.downloading_fallback")) # Fallback status
return # Don't process this line further for speed/ETA
# Check for specific download types in the output
if "Downloading video" in line:
self.status_signal.emit(_("download.downloading_video"))
return
elif "Downloading audio" in line:
self.status_signal.emit(_("download.downloading_audio"))
return
# Detect subtitle file creation
# Look for lines like "[info] Writing video subtitles to: filename.xx.vtt"
subtitle_match = re.search(
r"(?:Writing|Downloading) (?:video )?subtitles.*?(?:to|:)\s*(.+\.(?:vtt|srt))(?:\s|$)",
line,
re.IGNORECASE,
)
if subtitle_match:
subtitle_file = subtitle_match.group(1).strip()
# Clean up the path - remove any duplicated directory paths
# Sometimes yt-dlp output contains malformed paths like "dir: dir/file"
if ":" in subtitle_file and os.name == "nt": # Windows paths
# Look for pattern like "C:\path: C:\path\file" and extract the latter
colon_parts = subtitle_file.split(": ")
if len(colon_parts) > 1:
# Take the last part which should be the actual file path
subtitle_file = colon_parts[-1].strip()
# Show subtitle download message
self.status_signal.emit(_("download.downloading_subtitle"))
# Store the subtitle file path for later deletion if merging is enabled
if self.merge_subs:
subtitle_path = Path(subtitle_file)
if not subtitle_path.is_absolute():
# If it's a relative path, make it absolute based on current path
subtitle_path = self.path.joinpath(subtitle_file)
self.subtitle_files.append(str(subtitle_path))
logger.debug(f"Tracking subtitle file for later cleanup: {subtitle_path}")
return
# Send status updates based on output line content
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)
elif "Downloading m3u8 information" in line:
self.status_signal.emit(_("download.preparing_streams"))
self.progress_signal.emit(0)
elif "[download] Downloading video " in line:
self.status_signal.emit(_("download.downloading_video"))
elif "[download] Downloading audio " in line:
self.status_signal.emit(_("download.downloading_audio"))
elif "Downloading format" in line:
# Try to detect if it's audio or video format
if " - audio only" in line:
self.status_signal.emit(_("download.downloading_audio"))
elif " - video only" in line:
self.status_signal.emit(_("download.downloading_video"))
else:
# Don't emit generic message - format is unclear
pass
# Look for download percentage
percent_match = re.search(r"(\d+\.\d+)%", line)
if percent_match:
try:
percent = float(percent_match.group(1))
self.progress_signal.emit(percent)
except (ValueError, IndexError):
pass
# Check for download speed and ETA
if "[download]" in line and "%" in line:
# Try to extract more detailed status info
try:
# Look for speed
speed_match = re.search(r"at\s+(\d+\.\d+[KMG]iB/s)", line)
speed_str = speed_match.group(1) if speed_match else "N/A"
# Look for ETA
eta_match = re.search(r"ETA\s+(\d+:\d+)", line)
eta_str = eta_match.group(1) if eta_match else "N/A"
# Simplify status message to only show the speed and ETA
status = f"{_('download.speed')}: {speed_str} | {_('download.eta')}: {eta_str}"
self.update_details.emit(status)
except Exception as e:
# If parsing fails, just show basic status (maybe log the error)
logger.exception(f"Error parsing download details line: {line} -> {e}")
pass # Keep basic status emission below if needed, or emit generic details
# Check for post-processing
if "[Merger]" in line or "Merging formats" in line:
self.status_signal.emit(_("download.merging_formats"))
self.progress_signal.emit(95)
# Extract the merged output filename
merger_match = re.search(r"Merging formats into \"(.+?)\"", line)
if merger_match:
merged_filepath = merger_match.group(1).strip()
self.current_filename = Path(merged_filepath).name
self.last_file_path = merged_filepath
logger.debug(f"Updated to merged filename: {self.current_filename}")
elif "SponsorBlock" in line:
self.status_signal.emit(_("download.removing_sponsor_segments"))
self.progress_signal.emit(97)
elif "Deleting original file" in line:
self.progress_signal.emit(98)
elif "has already been downloaded" in line:
# File already exists - extract filename
match = re.search(r"(.*?) has already been downloaded", line)
if match:
filename = Path(match.group(1)).name
# Determine file type based on extension for existing file message
ext = Path(filename).suffix.lower()
if ext in VIDEO_EXTENSIONS:
self.status_signal.emit(f"⚠️ Video file already exists")
elif ext in AUDIO_EXTENSIONS:
self.status_signal.emit(f"⚠️ Audio file already exists")
elif ext in SUBTITLE_EXTENSIONS:
self.status_signal.emit(f"⚠️ Subtitle file already exists")
else:
self.status_signal.emit(f"⚠️ File already exists")
self.file_exists_signal.emit(filename)
else:
logger.info(f"Could not extract filename from 'already downloaded' line: {line}")
self.status_signal.emit(_("download.file_exists")) # Fallback status
elif "Finished downloading" in line:
self.progress_signal.emit(100)
# Show completion message based on file type
if self.current_filename:
ext = Path(self.current_filename).suffix.lower()
# Video file extensions
if ext in VIDEO_EXTENSIONS:
self.status_signal.emit(_("download.video_completed"))
# Audio file extensions
elif ext in AUDIO_EXTENSIONS:
self.status_signal.emit(_("download.audio_completed"))
# Subtitle file extensions
elif ext in SUBTITLE_EXTENSIONS:
self.status_signal.emit(_("download.subtitle_completed"))
# Default case
else:
self.status_signal.emit(_("download.completed"))
else:
self.status_signal.emit(_("download.completed"))
self.update_details.emit("") # Clear details label on completion
def pause(self) -> None:
self.paused = True
def resume(self) -> None:
self.paused = False
def cancel(self) -> None:
self.cancelled = True
# Terminate the subprocess if it's running
if self.process:
try:
self.process.terminate()
except Exception:
pass
+467
View File
@@ -0,0 +1,467 @@
import hashlib
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
import requests
from ..utils.ytsage_logger import logger
from ..utils.ytsage_constants import (
FFMPEG_7Z_DOWNLOAD_URL,
FFMPEG_7Z_SHA256_URL,
FFMPEG_ZIP_DOWNLOAD_URL,
FFMPEG_ZIP_SHA256_URL,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
)
def check_7zip_installed() -> bool:
"""Check if 7-Zip is installed on Windows."""
try:
subprocess.run(["7z", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=SUBPROCESS_CREATIONFLAGS)
return True
except (subprocess.SubprocessError, FileNotFoundError):
return False
def download_file(url, dest_path, progress_callback=None) -> bool:
"""Download a file from URL to destination path with progress indication."""
try:
response = requests.get(url, stream=True, timeout=30) # Added timeout
response.raise_for_status() # Check for HTTP errors
total_size = int(response.headers.get("content-length", 0))
with open(dest_path, "wb") as f:
if total_size == 0:
f.write(response.content)
else:
downloaded = 0
for data in response.iter_content(chunk_size=8192):
downloaded += len(data)
f.write(data)
if progress_callback:
progress = int((downloaded / total_size) * 100)
progress_callback(f"⚡ Downloading FFmpeg components... {progress}%")
return True
except requests.RequestException as e:
logger.info(f"Download error: {e}")
return False
def get_file_sha256(file_path) -> str:
"""Calculate SHA-256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
def verify_sha256(file_path, expected_hash_url) -> bool:
"""Verify file SHA-256 hash against expected hash from URL."""
try:
# Download the SHA-256 hash
response = requests.get(expected_hash_url, timeout=10)
response.raise_for_status()
expected_hash = response.text.strip().split()[0] # Get just the hash part
# Calculate actual hash
actual_hash = get_file_sha256(file_path)
# Compare hashes
if actual_hash.lower() == expected_hash.lower():
logger.info("SHA-256 verification successful!")
return True
else:
logger.error(f"SHA-256 verification failed!")
logger.info(f"Expected: {expected_hash}")
logger.info(f"Actual: {actual_hash}")
return False
except Exception as e:
logger.info(f"⚠️ SHA-256 verification error: {e}")
return False
def get_ffmpeg_install_path() -> Path:
"""
Get the FFmpeg installation path.
For Windows, tries to find the latest essentials build dynamically.
"""
if OS_NAME == "Windows":
ffmpeg_base = Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore
# If the directory exists, look for any ffmpeg-*-essentials_build folder
if ffmpeg_base.exists():
# Find all directories matching the pattern
essentials_dirs = list(ffmpeg_base.glob("ffmpeg-*-essentials_build"))
if essentials_dirs:
# Sort by name (which includes version) and take the latest
latest_dir = sorted(essentials_dirs, reverse=True)[0]
bin_dir = latest_dir / "bin"
if bin_dir.exists():
return bin_dir
# Fallback: return default path (even if it doesn't exist yet)
return ffmpeg_base / "ffmpeg-essentials_build" / "bin"
elif OS_NAME == "Darwin":
paths = ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"]
for path in paths:
if Path(path).joinpath("ffmpeg").exists():
return Path(path)
return Path("/usr/local/bin") # Default Homebrew path
else:
return Path("/usr/bin") # Standard Linux path
def get_ffmpeg_path() -> str | Path:
"""
Get the FFmpeg executable path, either from PATH or installation directory.
Returns:
str: Path to FFmpeg executable or 'ffmpeg' if found in PATH but path unknown
"""
try:
# First try to find ffmpeg in PATH using 'where' on Windows or 'which' on Unix
if OS_NAME == "Windows":
# On Windows, use 'where' command and hide console window
# Extra logic moved to src\utils\ytsage_constants.py
result = subprocess.run(
["where", "ffmpeg"],
capture_output=True,
text=True,
check=False,
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if result.returncode == 0 and result.stdout.strip():
ffmpeg_path = result.stdout.strip().split("\n")[0]
return ffmpeg_path
else:
# On Unix systems, use 'which' command
result = subprocess.run(["which", "ffmpeg"], capture_output=True, text=True, check=False)
if result.returncode == 0 and result.stdout.strip():
ffmpeg_path = result.stdout.strip()
return ffmpeg_path
except Exception as e:
logger.exception(f"Error finding ffmpeg in PATH: {e}")
# If not found in PATH, check the installation directory
ffmpeg_install_path = get_ffmpeg_install_path()
if OS_NAME == "Windows":
ffmpeg_exe = Path(ffmpeg_install_path).joinpath("ffmpeg.exe")
else:
ffmpeg_exe = Path(ffmpeg_install_path).joinpath("ffmpeg")
if ffmpeg_exe.exists():
return ffmpeg_exe
# Return command name as fallback
return "ffmpeg"
def check_ffmpeg_installed() -> bool:
"""Check if FFmpeg is installed and accessible."""
try:
# First try the PATH
result = subprocess.run(
["ffmpeg", "-version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=5,
) # Added timeout
return True
except (subprocess.SubprocessError, FileNotFoundError):
# If not in PATH, check the installation directory
ffmpeg_path = get_ffmpeg_install_path()
if OS_NAME == "Windows":
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg.exe")
else:
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg")
if ffmpeg_exe.exists():
# Add to PATH if found
os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
return True
return False
except Exception as e:
logger.info(f"FFmpeg check error: {e}")
return False
def install_ffmpeg_windows(progress_callback=None) -> bool:
"""Install FFmpeg on Windows using essentials build with 7z method primarily, with zip as fallback."""
# Check if already installed
if check_ffmpeg_installed():
logger.info("FFmpeg is already installed!")
if progress_callback:
progress_callback("✅ FFmpeg is already installed!")
return True
try:
# Define variables for essentials build
extract_dir = Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore
# Create extraction directory if it doesn't exist
extract_dir.mkdir(exist_ok=True)
# Try 7z method first (smaller size)
use_7zip = check_7zip_installed()
success = False
if use_7zip:
logger.info("Using 7-Zip method (smaller download size)...")
if progress_callback:
progress_callback("⚡ Using 7-Zip method...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".7z").name
# Download 7z file
if progress_callback:
progress_callback("⬇ Downloading FFmpeg (7z)...")
if download_file(
FFMPEG_7Z_DOWNLOAD_URL,
temp_file,
progress_callback=progress_callback,
):
# Verify SHA-256 hash for 7z file
if progress_callback:
progress_callback("🔐 Verifying download integrity...")
if verify_sha256(temp_file, FFMPEG_7Z_SHA256_URL):
logger.info("Extracting FFmpeg components from 7z archive...")
if progress_callback:
progress_callback("⚙ Extracting FFmpeg...")
try:
subprocess.run(
["7z", "x", temp_file, f"-o{extract_dir}", "-y"],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=300,
check=True,
)
success = True
except Exception as e:
logger.exception(f"7z extraction failed: {e}, trying zip fallback...")
if progress_callback:
progress_callback("❌ 7z extraction failed, trying zip fallback...")
else:
logger.error("SHA-256 verification failed for 7z file, trying zip fallback...")
if progress_callback:
progress_callback("❌ SHA-256 verification failed, trying zip fallback...")
# Clean up temp file
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
# Fallback to zip method if 7z failed or not available
if not success:
logger.info("Using ZIP method as fallback...")
if progress_callback:
progress_callback("📦 Using ZIP method...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip").name
# Download zip file
if progress_callback:
progress_callback("⬇ Downloading FFmpeg (zip)...")
if not download_file(
FFMPEG_ZIP_DOWNLOAD_URL,
temp_file,
progress_callback=progress_callback,
):
logger.error("Failed to download FFmpeg (both 7z and zip methods failed)")
if progress_callback:
progress_callback("❌ Failed to download FFmpeg")
return False
# Verify SHA-256 hash for zip
if progress_callback:
progress_callback("🔐 Verifying download integrity...")
if not verify_sha256(temp_file, FFMPEG_ZIP_SHA256_URL):
logger.warning("SHA-256 verification failed for zip file, proceeding anyway...")
if progress_callback:
progress_callback("⚠️ SHA-256 verification failed, proceeding anyway...")
logger.info("Extracting FFmpeg components from zip archive...")
if progress_callback:
progress_callback("⚙ Extracting FFmpeg...")
try:
import zipfile
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(extract_dir)
success = True
except Exception as e:
logger.exception(f"Extraction failed: {e}")
if progress_callback:
progress_callback(f"❌ Extraction failed: {e}")
return False
finally:
# Clean up temp file
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
if not success:
logger.error("Both 7z and zip methods failed")
if progress_callback:
progress_callback("❌ Installation failed")
return False
# Find the bin directory in the extracted essentials build
logger.info("Locating FFmpeg binaries...")
if progress_callback:
progress_callback("🔍 Locating FFmpeg binaries...")
bin_dir = None
# Look for any directory matching ffmpeg-*-essentials_build pattern
for item in extract_dir.iterdir():
if item.is_dir() and "essentials_build" in item.name.lower():
potential_bin = item / "bin"
if potential_bin.exists():
bin_dir = potential_bin
logger.info(f"Found FFmpeg bin directory: {bin_dir}")
break
if not bin_dir:
logger.error("Could not locate FFmpeg bin directory")
if progress_callback:
progress_callback("❌ Could not locate FFmpeg bin directory")
return False
logger.info("Configuring system paths...")
if progress_callback:
progress_callback("🔧 Configuring system paths...")
# Add to System Path
user_path = os.environ.get("PATH", "")
path_parts = user_path.split(os.pathsep)
# Remove old FFmpeg paths and add new one
cleaned_paths = [p for p in path_parts if "ffmpeg" not in p.lower() or str(bin_dir) in p]
if str(bin_dir) not in cleaned_paths:
cleaned_paths.insert(0, str(bin_dir))
new_path = os.pathsep.join(cleaned_paths)
try:
subprocess.run(
["setx", "PATH", new_path],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=30,
check=True,
)
os.environ["PATH"] = new_path
except Exception as e:
logger.warning(f"Failed to update PATH permanently: {e}")
# Still update for current session
os.environ["PATH"] = new_path
# Verify installation
if progress_callback:
progress_callback("✅ Verifying installation...")
if not check_ffmpeg_installed():
logger.error("FFmpeg installation verification failed")
if progress_callback:
progress_callback("⚠️ Installation completed but verification failed")
return True # Still return True as files were extracted
logger.info("FFmpeg installation completed successfully!")
if progress_callback:
progress_callback("✅ FFmpeg installation completed successfully!")
return True
except Exception as e:
logger.exception(f"Error installing FFmpeg: {e}")
if progress_callback:
progress_callback(f"❌ Error installing FFmpeg: {e}")
return False
def install_ffmpeg_macos() -> bool:
"""Install FFmpeg on macOS using Homebrew."""
try:
# Check if Homebrew is installed
try:
subprocess.run(
["brew", "--version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
timeout=5,
)
except (subprocess.SubprocessError, FileNotFoundError):
logger.info("Installing Homebrew...")
brew_install_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
subprocess.run(brew_install_cmd, shell=True, check=True, timeout=300)
# Install FFmpeg
logger.info("Installing FFmpeg...")
subprocess.run(["brew", "install", "ffmpeg"], check=True, timeout=300)
# Verify installation
if not check_ffmpeg_installed():
logger.error("FFmpeg installation verification failed")
return False
return True
except Exception as e:
logger.exception(f"Error installing FFmpeg: {e}")
return False
def install_ffmpeg_linux() -> bool:
"""Install FFmpeg on Linux using appropriate package manager."""
try:
# Detect the package manager
if shutil.which("apt"):
# Debian/Ubuntu
subprocess.run(["sudo", "apt", "update"], check=True, timeout=60)
subprocess.run(["sudo", "apt", "install", "-y", "ffmpeg"], check=True, timeout=300)
elif shutil.which("dnf"):
# Fedora
subprocess.run(["sudo", "dnf", "install", "-y", "ffmpeg"], check=True, timeout=300)
elif shutil.which("pacman"):
# Arch Linux
subprocess.run(
["sudo", "pacman", "-S", "--noconfirm", "ffmpeg"],
check=True,
timeout=300,
)
elif shutil.which("snap"):
# Universal snap package
subprocess.run(["sudo", "snap", "install", "ffmpeg"], check=True, timeout=300)
else:
logger.error("No supported package manager found")
return False
# Verify installation
if not check_ffmpeg_installed():
logger.error("FFmpeg installation verification failed")
return False
return True
except Exception as e:
logger.exception(f"Error installing FFmpeg: {e}")
return False
def auto_install_ffmpeg(progress_callback=None) -> bool:
"""Automatically install FFmpeg based on the operating system."""
if OS_NAME == "Windows":
return install_ffmpeg_windows(progress_callback=progress_callback)
elif OS_NAME == "Darwin":
return install_ffmpeg_macos()
elif OS_NAME == "Linux":
return install_ffmpeg_linux()
else:
logger.info(f"Unsupported operating system: {OS_NAME}")
if progress_callback:
progress_callback(f"❌ Unsupported operating system: {OS_NAME}")
return False
+835
View File
@@ -0,0 +1,835 @@
import json
import os
import subprocess
import sys
import tempfile
import time
from importlib.metadata import PackageNotFoundError
from pathlib import Path
from typing import Any, Dict, Optional, Union
import requests
from packaging import version
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,
USER_HOME_DIR,
YTDLP_APP_BIN_PATH,
YTDLP_DOWNLOAD_URL,
)
from ..utils.ytsage_localization import _
from ..utils.ytsage_logger import logger
try:
from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError
from importlib.metadata import version as importlib_version
def get_version(package_name: str) -> str:
return importlib_version(package_name)
PackageNotFoundError = ImportlibPackageNotFoundError
except ImportError:
# Fallback for older Python versions
import pkg_resources
def get_version(package_name: str) -> str:
return pkg_resources.get_distribution(package_name).version
PackageNotFoundError = pkg_resources.DistributionNotFound
# Cache for version information to avoid delays
_version_cache: Dict[str, Dict[str, Any]] = {
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"deno": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
}
# Cache expiry time in seconds (5 minutes)
CACHE_EXPIRY: int = 300
def get_file_mtime(filepath: Optional[Union[str, Path]]) -> float:
"""Get file modification time safely."""
try:
if filepath and Path(filepath).exists():
return Path(filepath).stat().st_mtime
except Exception:
pass
return 0.0
def should_refresh_cache(tool_name: str, current_path: Optional[str]) -> bool:
"""Determine if cache should be refreshed for a tool."""
cache: Dict[str, Any] = _version_cache.get(tool_name, {})
current_time: float = time.time()
# Always refresh if no cached data
if not cache.get("version"):
return True
# Refresh if path changed
if cache.get("path") != current_path:
return True
# Refresh if file was modified
current_mtime = get_file_mtime(current_path)
if current_mtime > cache.get("path_mtime", 0):
return True
# Refresh if cache expired
if current_time - cache.get("last_check", 0) > CACHE_EXPIRY:
return True
return False
def update_version_cache(tool_name: str, version_info: str, path: Optional[str], force_save: bool = False) -> None:
"""Update the version cache and optionally save to config."""
current_time: float = time.time()
current_mtime: float = get_file_mtime(path)
_version_cache[tool_name] = {
"version": str(version_info) if version_info else "",
"path": str(path) if path else None,
"last_check": current_time,
"path_mtime": current_mtime,
}
# Save to persistent config
if force_save:
save_version_cache_to_config()
def load_version_cache_from_config() -> None:
"""Load cached version info from config file."""
from ..utils.ytsage_config_manager import ConfigManager
try:
cached_versions = ConfigManager.get("cached_versions") or {}
for tool_name, cache_data in cached_versions.items():
if tool_name in _version_cache:
_version_cache[tool_name].update(cache_data)
except Exception as e:
logger.exception(f"Error loading version cache: {e}")
def save_version_cache_to_config() -> None:
"""Save version cache to config file."""
from ..utils.ytsage_config_manager import ConfigManager
try:
ConfigManager.set("cached_versions", _version_cache.copy())
except Exception as e:
logger.exception(f"Error saving version cache: {e}")
def get_ytdlp_version_cached() -> str:
"""Get yt-dlp version with caching support."""
try:
current_path = get_yt_dlp_path()
# Check if we need to refresh cache
if not should_refresh_cache("ytdlp", current_path):
cached_version = _version_cache["ytdlp"].get("version")
if cached_version:
return cached_version
# Get fresh version info
version_info = get_ytdlp_version_direct(current_path)
# Update cache
update_version_cache("ytdlp", version_info, current_path)
return version_info
except Exception as e:
logger.exception(f"Error getting cached yt-dlp version: {e}")
return "Error getting version"
def get_ffmpeg_version_cached() -> str:
"""Get FFmpeg version with caching support."""
try:
# Try to find ffmpeg path
current_path = "ffmpeg" # Default to system PATH
# Check if we need to refresh cache
if not should_refresh_cache("ffmpeg", current_path):
cached_version = _version_cache["ffmpeg"].get("version")
if cached_version:
return cached_version
# Get fresh version info
version_info = get_ffmpeg_version_direct()
# Update cache
update_version_cache("ffmpeg", version_info, current_path)
return version_info
except Exception as e:
logger.exception(f"Error getting cached FFmpeg version: {e}")
return "Error getting version"
def get_deno_version_cached() -> str:
"""Get Deno version with caching support."""
try:
from .ytsage_deno import get_deno_path
current_path = get_deno_path()
# Check if we need to refresh cache
if not should_refresh_cache("deno", current_path):
cached_version = _version_cache["deno"].get("version")
if cached_version:
return cached_version
# Get fresh version info
from .ytsage_deno import get_deno_version_direct
version_info = get_deno_version_direct(current_path)
# Update cache
update_version_cache("deno", version_info, current_path)
return version_info
except Exception as e:
logger.exception(f"Error getting cached Deno version: {e}")
return "Error getting version"
def refresh_version_cache(force=False) -> bool:
"""Manually refresh version cache for all tools."""
try:
# Refresh yt-dlp
current_path = get_yt_dlp_path()
version_info = get_ytdlp_version_direct(current_path)
update_version_cache("ytdlp", version_info, current_path, force_save=True)
# Refresh FFmpeg
version_info = get_ffmpeg_version_direct()
update_version_cache("ffmpeg", version_info, "ffmpeg", force_save=True)
# Refresh Deno
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)
return True
except Exception as e:
logger.exception(f"Error refreshing version cache: {e}")
return False
def get_ytdlp_version() -> str:
"""Get the version of yt-dlp (uses cached version for performance)."""
return get_ytdlp_version_cached()
def get_ffmpeg_version() -> str:
"""Get the version of FFmpeg (uses cached version for performance)."""
return get_ffmpeg_version_cached()
def get_deno_version() -> str:
"""Get the version of Deno (uses cached version for performance)."""
return get_deno_version_cached()
def get_ytdlp_version_direct(yt_dlp_path: Optional[str] = None) -> str:
"""Get yt-dlp version directly without caching."""
try:
if yt_dlp_path is None:
yt_dlp_path = get_yt_dlp_path()
if not yt_dlp_path or yt_dlp_path == "yt-dlp":
return "Not found"
# Extra logic moved to src\utils\ytsage_constants.py
result = subprocess.run(
[yt_dlp_path, "--version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS
)
if result.returncode == 0:
return result.stdout.strip()
else:
return "Error getting version"
except Exception as e:
logger.exception(f"Error getting yt-dlp version: {e}")
return "Error getting version"
def get_ffmpeg_version_direct() -> str:
"""Get FFmpeg version directly without caching."""
try:
# Extra logic moved to src\utils\ytsage_constants.py
result = subprocess.run(
["ffmpeg", "-version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS
)
if result.returncode == 0:
# Parse the first line to get version info
lines = result.stdout.split("\n")
if lines:
first_line = lines[0]
# Extract version from something like "ffmpeg version 4.4.2 Copyright..."
if "version" in first_line:
parts = first_line.split()
for i, part in enumerate(parts):
if part == "version" and i + 1 < len(parts):
return parts[i + 1]
return first_line.strip()
return "Unknown version"
else:
return "Not found"
except FileNotFoundError:
# If ffmpeg is not in PATH, try the installation directory
try:
ffmpeg_path = get_ffmpeg_install_path()
if OS_NAME == "Windows":
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg.exe")
else:
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg")
if ffmpeg_exe.exists():
result = subprocess.run(
[ffmpeg_exe, "-version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS
)
if result.returncode == 0:
lines = result.stdout.split("\n")
if lines:
first_line = lines[0]
if "version" in first_line:
parts = first_line.split()
for i, part in enumerate(parts):
if part == "version" and i + 1 < len(parts):
return parts[i + 1]
return first_line.strip()
return "Unknown version"
return "Not found"
except Exception as e:
logger.exception(f"Error getting FFmpeg version from install path: {e}")
return "Not found"
except Exception as e:
logger.exception(f"Error getting FFmpeg version: {e}")
return "Error getting version"
# 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
# load_config() and save_config() removed - use ConfigManager instead
def check_ffmpeg() -> bool:
"""Check if FFmpeg is installed and accessible with enhanced error handling."""
try:
# Use the enhanced FFmpeg check from ytsage_ffmpeg
if check_ffmpeg_installed():
return True
# For Windows, try to add the FFmpeg path to environment
if OS_NAME == "Windows":
ffmpeg_path = get_ffmpeg_install_path()
if ffmpeg_path.joinpath("ffmpeg.exe").exists():
try:
# Add to current session PATH
os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
return True
except Exception as e:
logger.exception(f"Error updating PATH: {e}")
return False
# For macOS, check common paths
elif OS_NAME == "Darwin":
common_paths = [
"/usr/local/bin/ffmpeg",
"/opt/homebrew/bin/ffmpeg",
"/usr/bin/ffmpeg",
]
for path in common_paths:
if Path(path).exists():
try:
ffmpeg_dir = Path(path).parent
os.environ["PATH"] = f"{ffmpeg_dir}{os.pathsep}{os.environ.get('PATH', '')}"
return True
except Exception as e:
logger.exception(f"Error updating PATH: {e}")
continue
return False
except Exception as e:
logger.exception(f"Error checking FFmpeg: {e}")
return False
def load_saved_path(main_window_instance: Any) -> None:
"""Load saved download path with enhanced error handling."""
try:
if APP_CONFIG_FILE.exists():
try:
with open(APP_CONFIG_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
saved_path = config.get("download_path", "")
if Path(saved_path).exists() and os.access(saved_path, os.W_OK):
main_window_instance.last_path = saved_path
return
except (json.JSONDecodeError, UnicodeError) as e:
logger.exception(f"Error reading config file: {e}")
# If config file is corrupted, try to remove it
try:
APP_CONFIG_FILE.unlink(missing_ok=True)
except Exception:
pass
# Fallback to Downloads folder
downloads_path = USER_HOME_DIR / "Downloads"
if downloads_path.exists() and os.access(downloads_path, os.W_OK):
main_window_instance.last_path = downloads_path
else:
# Final fallback to temp directory if Downloads is not accessible
main_window_instance.last_path = tempfile.gettempdir()
except Exception as e:
logger.exception(f"Error loading saved settings: {e}")
main_window_instance.last_path = tempfile.gettempdir()
from ..utils.ytsage_config_manager import ConfigManager
def save_path(main_window_instance: Any, path: Union[str, Path]) -> bool:
"""Save download path with enhanced error handling."""
try:
# Verify the path is valid and writable
path_str = str(path)
if not Path(path_str).exists():
try:
Path(path_str).mkdir(parents=True, exist_ok=True)
except Exception as e:
logger.error(f"Error creating directory: {e}")
return False
if not os.access(path_str, os.W_OK):
logger.error("Path is not writable")
return False
# Save the config using ConfigManager
ConfigManager.set("download_path", path_str)
return True
except Exception as e:
logger.exception(f"Error saving settings: {e}")
return False
def update_yt_dlp() -> bool:
"""Check for yt-dlp updates and update if a newer version is available."""
try:
# Get the yt-dlp path
yt_dlp_path: Path = get_yt_dlp_path()
# Extra logic moved to src\utils\ytsage_constants.py
# For binaries downloaded with our app, use direct binary update approach
# Check if this is an app-managed binary by comparing paths safely
is_app_managed: bool = False
try:
# Only compare if both files exist
if yt_dlp_path.exists() and YTDLP_APP_BIN_PATH.exists():
is_app_managed = yt_dlp_path.samefile(YTDLP_APP_BIN_PATH)
elif str(yt_dlp_path) == str(YTDLP_APP_BIN_PATH):
# If paths are identical as strings, consider it app-managed
is_app_managed = True
else:
# If app binary doesn't exist, this is definitely not app-managed
is_app_managed = False
except (OSError, IOError) as e:
logger.debug(f"Error comparing paths in update_yt_dlp: {e}")
is_app_managed = False
if is_app_managed:
# We're using a binary installed by our app, update directly
logger.info(f"Updating yt-dlp binary at {yt_dlp_path}")
# Determine the URL based on OS
# Extra logic moved to src\utils\ytsage_constants.py
# Download the latest version
try:
response = requests.get(YTDLP_DOWNLOAD_URL, stream=True)
if response.status_code == 200:
# Create a temporary file
temp_file = f"{yt_dlp_path}.new"
with open(temp_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# Make executable on Unix systems
if OS_NAME != "Windows":
os.chmod(temp_file, 0o755)
# Replace the old file with the new one
try:
# On Windows, we need to remove the old file first
if OS_NAME == "Windows" and yt_dlp_path.exists():
yt_dlp_path.unlink(missing_ok=True)
Path(temp_file).rename(yt_dlp_path)
logger.info("yt-dlp binary successfully updated")
return True
except Exception as e:
logger.exception(f"Error replacing yt-dlp binary: {e}")
return False
else:
logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}")
return False
except Exception as e:
logger.exception(f"Error downloading yt-dlp update: {e}")
return False
else:
# We're using a system-installed yt-dlp, use pip to update
logger.info("Using pip to update yt-dlp")
# Get current version
try:
current_version = get_version("yt-dlp")
logger.info(f"Current yt-dlp version: {current_version}")
except PackageNotFoundError:
logger.info("yt-dlp not installed via pip, attempting update anyway")
current_version = "0.0.0" # Assume very old version to force update
# Get the latest version from PyPI JSON API
try:
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
if response.status_code == 200:
data = response.json()
latest_version = data["info"]["version"]
logger.info(f"Latest available yt-dlp version: {latest_version}")
# Compare versions and update if needed
if version.parse(latest_version) > version.parse(current_version):
logger.info(f"Updating yt-dlp from {current_version} to {latest_version}...")
update_result = subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"--upgrade",
"yt-dlp",
],
capture_output=True,
text=True,
check=False,
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if update_result.returncode == 0:
logger.info("yt-dlp successfully updated")
return True
else:
logger.error(f"Error updating yt-dlp: {update_result.stderr}")
else:
logger.info("yt-dlp is already up to date")
return True
else:
logger.info(f"Failed to get latest version info: HTTP {response.status_code}")
except Exception as e:
logger.exception(f"Error checking for yt-dlp updates: {e}")
except Exception as e:
logger.exception(f"Unexpected error during yt-dlp update: {e}")
return False
def should_check_for_auto_update() -> bool:
"""Check if auto-update should be performed based on user settings."""
from ..utils.ytsage_config_manager import ConfigManager
try:
# Check if auto-update is enabled
if not ConfigManager.get("auto_update_ytdlp"):
return False
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
time_diff: float = current_time - last_check
if frequency == "startup":
# Always check on startup if we haven't checked in the last hour
return time_diff > 3600 # 1 hour
elif frequency == "daily":
return time_diff > 86400 # 24 hours
elif frequency == "weekly":
return time_diff > 604800 # 7 days
return False
except Exception as e:
logger.exception(f"Error checking auto-update schedule: {e}")
return False
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...")
# Get current version
current_version = get_ytdlp_version()
if "Error" in current_version:
logger.info("Could not determine current yt-dlp version, skipping auto-update")
return False
# Get latest version from PyPI
try:
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
response.raise_for_status()
latest_version = response.json()["info"]["version"]
# Clean up version strings
current_version = current_version.replace("_", ".")
latest_version = latest_version.replace("_", ".")
logger.info(f"Current yt-dlp version: {current_version}")
logger.info(f"Latest yt-dlp version: {latest_version}")
# Compare versions
if version.parse(latest_version) > version.parse(current_version):
logger.info(f"Auto-updating yt-dlp from {current_version} to {latest_version}...")
# Perform the update
if update_yt_dlp():
logger.info("Auto-update completed successfully!")
# Update the last check timestamp
ConfigManager.set("last_update_check", time.time())
return True
else:
logger.info("Auto-update failed")
return False
else:
logger.info("yt-dlp is already up to date")
# Still update the timestamp even if no update was needed
ConfigManager.set("last_update_check", time.time())
return True
except requests.RequestException as e:
logger.info(f"Network error during auto-update check: {e}")
return False
except Exception as e:
logger.exception(f"Error during auto-update check: {e}")
return False
except Exception as e:
logger.critical(f"Critical error in auto-update: {e}", exc_info=True)
return False
def get_auto_update_settings() -> Dict[str, Any]:
"""Get current auto-update settings from config."""
from ..utils.ytsage_config_manager import ConfigManager
enabled: Optional[bool] = ConfigManager.get("auto_update_ytdlp")
frequency: Optional[str] = ConfigManager.get("auto_update_frequency")
last_check: Optional[float] = ConfigManager.get("last_update_check")
return {
"enabled": enabled if enabled is not None else True,
"frequency": frequency if frequency is not None else "daily",
"last_check": last_check if last_check is not None else 0,
}
def update_auto_update_settings(enabled: bool, frequency: str) -> bool:
"""Update auto-update settings in config."""
try:
from ..utils.ytsage_config_manager import ConfigManager
ConfigManager.set("auto_update_ytdlp", enabled)
ConfigManager.set("auto_update_frequency", frequency)
return True
except Exception as e:
logger.exception(f"Error updating auto-update settings: {e}")
return False
def parse_yt_dlp_error(error_message: str) -> str:
"""
Parse yt-dlp error messages and return user-friendly error messages.
Args:
error_message: The raw error message from yt-dlp
Returns:
str: A user-friendly error message with actionable advice
"""
error_str = error_message.lower()
# Private video errors
if any(keyword in error_str for keyword in ["private video", "login_required", "sign in if you"]):
return _("ytdlp_errors.private_video")
# Age-restricted content
if any(keyword in error_str for keyword in ["age restricted", "age-restricted", "confirm your age"]):
return _("ytdlp_errors.age_restricted")
# Geo-blocked content
if any(
keyword in error_str
for keyword in [
"not available in your country",
"geo-blocked",
"video is not available",
"not made this video available in your country",
]
):
return _("ytdlp_errors.geo_blocked")
# Removed/deleted videos
if any(keyword in error_str for keyword in ["video unavailable", "this video has been removed", "video does not exist"]):
return _("ytdlp_errors.video_unavailable")
# Live stream errors
if any(keyword in error_str for keyword in ["live stream", "livestream", "is live"]):
return _("ytdlp_errors.live_stream")
# Playlist errors
if any(keyword in error_str for keyword in ["playlist", "no entries"]):
return _("ytdlp_errors.playlist_error")
# Network/connection errors
if any(keyword in error_str for keyword in ["network error", "connection", "timeout", "unable to download"]):
return _("ytdlp_errors.network_error")
# Invalid URL
if any(keyword in error_str for keyword in ["invalid url", "unsupported url", "no video found"]):
return _("ytdlp_errors.invalid_url")
# YouTube premium content
if any(keyword in error_str for keyword in ["youtube premium", "premium", "members only"]):
return _("ytdlp_errors.premium_content")
# Copyright/DMCA
if any(keyword in error_str for keyword in ["copyright", "dmca", "blocked"]):
return _("ytdlp_errors.copyright_blocked")
# Extraction errors (could be temporary)
if any(keyword in error_str for keyword in ["unable to extract", "extraction failed"]):
return _("ytdlp_errors.extraction_failed")
# Generic fallback with the original error for debugging
return _("ytdlp_errors.generic_error", error=error_message)
def validate_video_url(url: str, generic_mode: bool = False) -> tuple[bool, str]:
"""
Validate a video URL for supported platforms.
Args:
url: The URL string to validate
Returns:
tuple[bool, str]: (is_valid, error_message)
- is_valid: True if URL is valid, False otherwise
- error_message: Empty string if valid, error description if invalid
Args:
url: URL entered by the user.
generic_mode: When True, allow any http/https URL with a valid domain.
Example:
>>> is_valid, error = validate_video_url("https://youtube.com/watch?v=xxx")
>>> if not is_valid:
... print(error)
"""
from urllib.parse import urlparse
# Check if URL is empty
if not url or not url.strip():
return False, _("url_validation.empty_url")
url = url.strip()
# Check basic URL structure
try:
parsed = urlparse(url)
except Exception as e:
logger.debug(f"URL parsing error: {e}")
return False, _("url_validation.invalid_format")
# Check if scheme is http or https
if parsed.scheme not in ['http', 'https']:
return False, _("url_validation.invalid_scheme")
# Check if netloc (domain) exists
if not parsed.netloc:
return False, _("url_validation.missing_domain")
if generic_mode:
logger.info(f"Generic mode enabled, allowing URL: {url}")
return True, ""
# YTSage focuses on YouTube and YouTube Music only
# Supported YouTube domains
youtube_domains = [
'youtube.com',
'www.youtube.com',
'youtu.be',
'm.youtube.com',
'music.youtube.com', # YouTube Music
'gaming.youtube.com', # YouTube Gaming (redirects to main)
]
# Check if domain is YouTube
netloc_lower = parsed.netloc.lower()
is_youtube = any(
netloc_lower == domain or netloc_lower.endswith('.' + domain)
for domain in youtube_domains
)
if not is_youtube:
return False, _("url_validation.unsupported_platform", domain=parsed.netloc)
# Optional: Validate YouTube URL patterns
# Common YouTube URL patterns:
# - /watch?v=VIDEO_ID
# - /playlist?list=PLAYLIST_ID
# - /shorts/VIDEO_ID
# - youtu.be/VIDEO_ID
valid_patterns = [
'/watch',
'/playlist',
'/shorts/',
'/live/',
'/channel/',
'/c/',
'/user/',
'@', # New handle format
]
# For youtu.be, the path itself is the video ID
if 'youtu.be' in netloc_lower:
if not parsed.path or parsed.path == '/':
return False, _("url_validation.invalid_youtu_be")
return True, ""
# For youtube.com domains, check for valid patterns
if any(pattern in url.lower() for pattern in valid_patterns):
return True, ""
# If it's a YouTube domain but doesn't match known patterns, still allow it
# (yt-dlp might support formats we don't know about)
logger.info(f"YouTube URL doesn't match known patterns but allowing: {url}")
return True, ""
+754
View File
@@ -0,0 +1,754 @@
import os
import shutil
import subprocess
from pathlib import Path
from typing import Optional
import requests
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QDialog,
QFileDialog,
QHBoxLayout,
QLabel,
QMessageBox,
QProgressBar,
QPushButton,
QRadioButton,
QVBoxLayout,
QWidget,
)
from ..utils.ytsage_logger import logger
from ..utils.ytsage_constants import (
APP_BIN_DIR,
ICON_PATH,
OS_FULL_NAME,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
YTDLP_APP_BIN_PATH,
YTDLP_DOWNLOAD_URL,
YTDLP_SHA256_URL,
)
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
# get_ytdlp_executable_path() moved to src\utils\ytsage_constants.py
# get_os_type() moved to src\utils\ytsage_constants.py
# ensure_install_dir_exists() moved to src\utils\ytsage_constants.py
def verify_ytdlp_sha256(file_path: Path, download_url: str) -> bool:
"""
Verify yt-dlp file SHA256 hash against official checksums.
Args:
file_path: Path to the downloaded yt-dlp file
download_url: The URL used to download the file (to determine the filename)
Returns:
bool: True if verification successful, False otherwise
"""
try:
# Download the SHA2-256SUMS file
logger.info(f"Downloading SHA256 checksums from: {YTDLP_SHA256_URL}")
response = requests.get(YTDLP_SHA256_URL, timeout=10)
response.raise_for_status()
checksum_content = response.text
# Extract filename from download URL (e.g., yt-dlp.exe, yt-dlp_macos, yt-dlp)
filename = download_url.split("/")[-1]
logger.info(f"Looking for checksum for file: {filename}")
# Parse the checksum file to find the matching hash
expected_hash = None
for line in checksum_content.strip().split("\n"):
if filename in line:
# Format: "hash filename"
parts = line.strip().split()
if len(parts) >= 2 and parts[1] == filename:
expected_hash = parts[0]
break
if not expected_hash:
logger.error(f"Could not find SHA256 hash for {filename} in checksums file")
return False
# Calculate actual hash of downloaded file
logger.info("Calculating SHA256 hash of downloaded file...")
actual_hash = get_file_sha256(file_path)
# Compare hashes
if actual_hash.lower() == expected_hash.lower():
logger.info("✓ SHA256 verification successful!")
logger.info(f" Expected: {expected_hash}")
logger.info(f" Actual: {actual_hash}")
return True
else:
logger.error("✗ SHA256 verification failed!")
logger.error(f" Expected: {expected_hash}")
logger.error(f" Actual: {actual_hash}")
return False
except requests.RequestException as e:
logger.error(f"Failed to download SHA256 checksums: {e}")
return False
except Exception as e:
logger.exception(f"Error during SHA256 verification: {e}")
return False
class DownloadYtdlpThread(QThread):
progress_signal = Signal(int)
finished_signal = Signal(bool, str)
def __init__(self):
super().__init__()
def run(self) -> None:
try:
# Extra logic moved to src\utils\ytsage_constants.py
exe_path = YTDLP_APP_BIN_PATH
# Download with progress reporting
logger.info(f"Downloading yt-dlp from: {YTDLP_DOWNLOAD_URL}")
response = requests.get(YTDLP_DOWNLOAD_URL, stream=True)
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
block_size = 1024 # 1 Kibibyte
if total_size == 0:
self.progress_signal.emit(100)
with open(exe_path, "wb") as f:
downloaded = 0
for data in response.iter_content(block_size):
f.write(data)
downloaded += len(data)
if total_size > 0:
progress = int(downloaded / total_size * 100)
self.progress_signal.emit(progress)
logger.info("Download complete, verifying SHA256 hash...")
# Verify SHA256 hash
if not verify_ytdlp_sha256(exe_path, YTDLP_DOWNLOAD_URL):
# Hash verification failed - delete the downloaded file
logger.error("SHA256 verification failed! Removing downloaded file.")
if Path(exe_path).exists():
Path(exe_path).unlink()
self.finished_signal.emit(
False,
"SHA256 verification failed. The downloaded file may be corrupted or tampered with."
)
return
# Make executable on macOS and Linux
if OS_NAME != "Windows":
os.chmod(exe_path, 0o755)
logger.info("yt-dlp downloaded and verified successfully!")
self.finished_signal.emit(True, str(exe_path))
except Exception as e:
logger.exception(f"Error downloading yt-dlp: {e}")
self.finished_signal.emit(False, str(e))
class YtdlpSetupDialog(QDialog):
setup_complete = Signal(str) # Signal emitting the path to yt-dlp
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle(_("ytdlp_setup.required_title"))
self.setMinimumWidth(520)
self.setMinimumHeight(350)
self.resize(520, 380)
# Set the window icon to match the main app
if parent and parent.windowIcon():
self.setWindowIcon(parent.windowIcon())
else:
# icon_path logic moved to src\utils\ytsage_constants.py
icon_path = ICON_PATH
if Path.exists(icon_path):
self.setWindowIcon(QIcon(str(icon_path)))
self.init_ui()
# Apply dark theme styling to match app
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #cccccc;
line-height: 1.4;
}
QPushButton {
padding: 10px 20px;
background-color: #c90000;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-width: 120px;
min-height: 20px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
QPushButton:disabled {
background-color: #666666;
color: #999999;
}
QProgressBar {
border: 2px solid #1d1e22;
border-radius: 6px;
text-align: center;
color: white;
background-color: #1d1e22;
height: 25px;
font-weight: bold;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #e60000, stop: 0.5 #ff3333, stop: 1 #c90000);
border-radius: 4px;
margin: 1px;
}
QRadioButton {
color: #ffffff;
spacing: 10px;
padding: 8px;
font-size: 13px;
}
QRadioButton::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QRadioButton::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
}
QRadioButton::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
}
"""
)
def init_ui(self) -> None:
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(25, 25, 25, 25)
# Header title
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)
# Information label with improved styling
# os_name logic moved to src\utils\ytsage_constants.py
info_label = QLabel(
_("ytdlp_setup.description", os_name=OS_FULL_NAME)
)
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
info_label.setWordWrap(True)
info_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 5px; line-height: 1.4;")
layout.addWidget(info_label)
# Radio buttons with minimal spacing
option_widget = QWidget()
option_layout = QVBoxLayout(option_widget)
option_layout.setSpacing(8)
option_layout.setContentsMargins(0, 0, 0, 0)
self.auto_radio = QRadioButton(_("ytdlp_setup.option_auto"))
self.auto_radio.setChecked(True)
self.manual_radio = QRadioButton(_("ytdlp_setup.option_manual"))
option_layout.addWidget(self.auto_radio)
option_layout.addWidget(self.manual_radio)
layout.addWidget(option_widget)
# Progress bar with proper sizing
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setFixedHeight(20) # Fixed height for consistency
self.progress_bar.setStyleSheet(
"""
QProgressBar {
border: 1px solid #3d3d3d;
border-radius: 8px;
background-color: #1d1e22;
text-align: center;
color: #ffffff;
font-size: 12px;
font-weight: bold;
height: 20px;
}
QProgressBar::chunk {
background-color: #c90000;
border-radius: 6px;
margin: 1px;
}
"""
)
layout.addWidget(self.progress_bar)
# Status label with better spacing
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("font-size: 12px; color: #cccccc; padding: 8px 0;")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
# Add stretch to push buttons to bottom
layout.addStretch()
# Button layout with improved spacing
button_layout = QHBoxLayout()
button_layout.setSpacing(15)
button_layout.setContentsMargins(0, 10, 0, 0) # Add top margin for buttons
self.setup_button = QPushButton(_("ytdlp_setup.setup_button"))
self.setup_button.clicked.connect(self.setup_ytdlp)
self.cancel_button = QPushButton(_("buttons.cancel"))
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.setup_button)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def setup_ytdlp(self) -> None:
if self.auto_radio.isChecked():
self.download_ytdlp()
else:
self.select_ytdlp_path()
def download_ytdlp(self) -> None:
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText(_("ytdlp_setup.downloading"))
self.setup_button.setEnabled(False)
self.cancel_button.setEnabled(False)
self.download_thread = DownloadYtdlpThread()
self.download_thread.progress_signal.connect(self.update_progress)
self.download_thread.finished_signal.connect(self.download_finished)
self.download_thread.start()
def update_progress(self, value) -> None:
self.progress_bar.setValue(value)
def download_finished(self, success, result) -> None:
self.setup_button.setEnabled(True)
self.cancel_button.setEnabled(True)
if success:
self.status_label.setText(_("ytdlp_setup.success"))
self.setup_complete.emit(result)
self.accept()
else:
self.status_label.setText(_("ytdlp_setup.error", error=result))
error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical)
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(
"""
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;
}
"""
)
error_dialog.exec()
def select_ytdlp_path(self) -> None:
if OS_NAME == "Windows":
file_filter = _("ytdlp_setup.file_filter_windows")
else:
file_filter = _("ytdlp_setup.file_filter_all")
# Apply style to QFileDialog
file_dialog = QFileDialog(self)
file_dialog.setStyleSheet(
"""
QFileDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel, QCheckBox, QListView, QTreeView, QComboBox, QLineEdit {
color: #ffffff;
background-color: #1b2021;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
"""
)
file_path, _ = file_dialog.getOpenFileName(
self, _("ytdlp_setup.select_executable_title"), "", file_filter
)
if file_path:
logger.debug(f"User selected file: {file_path}")
# Verify the selected file
try:
# Extra logic moved to src\utils\ytsage_constants.py
# Try to run yt-dlp --version
logger.debug(f"Verifying file with --version command")
result = subprocess.run(
[file_path, "--version"], capture_output=True, text=True, check=False, creationflags=SUBPROCESS_CREATIONFLAGS
)
logger.debug(f"Version check result: {result.returncode}, Output: {result.stdout.strip()}")
if result.returncode == 0:
# File is valid, copy it to our app's bin directory
try:
# Ensure the bin directory exists
logger.debug(f"Install directory: {APP_BIN_DIR}")
# Determine the target filename based on OS
target_path = YTDLP_APP_BIN_PATH
logger.debug(f"Target path: {target_path}")
# Copy the file
shutil.copy2(file_path, target_path)
logger.debug(f"File copied successfully")
# Set executable permissions on Unix systems
if OS_NAME != "Windows":
os.chmod(target_path, 0o755)
logger.debug(f"Permissions set on Unix system")
# Return the path of the copied file
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()
except Exception as copy_error:
logger.debug(f"Error copying file: {copy_error}", exc_info=True)
error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle(_("ytdlp_setup.setup_error_title"))
error_dialog.setText(_("ytdlp_setup.copy_error", error=copy_error))
error_dialog.setStyleSheet(
"""
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;
}
"""
)
error_dialog.exec()
else:
logger.debug(f"File verification failed with return code: {result.returncode}")
error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Warning)
error_dialog.setWindowTitle(_("ytdlp_setup.invalid_executable_title"))
error_dialog.setText(_("ytdlp_setup.invalid_executable_message"))
error_dialog.setStyleSheet(
"""
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;
}
"""
)
error_dialog.exec()
except Exception as e:
logger.debug(f"Exception during verification: {e}", exc_info=True)
error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle(_("main_ui.error_title"))
error_dialog.setText(_("ytdlp_setup.verify_error", error=e))
error_dialog.setStyleSheet(
"""
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;
}
"""
)
error_dialog.exec()
def check_ytdlp_binary() -> Optional[Path]:
"""
Check if yt-dlp binary exists in the app's bin directory ONLY.
We now ignore system PATH and only use our managed binary.
Returns:
Path or None: Path to yt-dlp binary if found in app bin, None otherwise
"""
exe_path = YTDLP_APP_BIN_PATH
if exe_path.exists():
# Make sure it's executable on Unix systems
if OS_NAME != "Windows" and not os.access(exe_path, os.X_OK):
try:
os.chmod(exe_path, 0o755)
logger.info(f"Fixed permissions on yt-dlp at {exe_path}")
except Exception as e:
logger.exception(f"Could not set executable permissions on {exe_path}: {e}")
logger.info(f"Found yt-dlp in app bin directory: {exe_path}")
return exe_path
# Binary not found in app directory - return None to trigger setup
logger.warning(f"yt-dlp binary not found in app bin directory: {exe_path}")
return None
def check_ytdlp_installed() -> bool:
"""
Check if yt-dlp is installed and accessible.
Returns:
bool: True if yt-dlp is found and working, False otherwise
"""
try:
ytdlp_path = check_ytdlp_binary()
if ytdlp_path:
# Try to run yt-dlp --version to verify it's working
try:
# Extra logic moved to src\utils\ytsage_constants.py
result = subprocess.run(
[ytdlp_path, "--version"], capture_output=True, text=True, timeout=5, creationflags=SUBPROCESS_CREATIONFLAGS
)
return result.returncode == 0
except Exception:
return False
return False
except Exception:
return False
def get_yt_dlp_path() -> Path:
"""
Get the yt-dlp path, either from the app's bin directory or system PATH.
This replaces the function in ytsage_utils.py.
Returns:
str: Path to yt-dlp binary
"""
# First check if we have yt-dlp in our app's bin directory or system PATH
ytdlp_path = check_ytdlp_binary()
if ytdlp_path:
logger.info(f"Using yt-dlp from: {ytdlp_path}")
return ytdlp_path
# If not found anywhere, fall back to the command name as a last resort
logger.info("yt-dlp not found in app directory or PATH, falling back to command name")
return "yt-dlp" # type: ignore[return-value]
def setup_ytdlp(parent_widget=None):
"""
Show the yt-dlp setup dialog and handle the result.
Returns:
str: Path to yt-dlp binary
"""
logger.debug("Starting yt-dlp setup dialog")
dialog = YtdlpSetupDialog(parent_widget)
# Store the setup result from the signal
setup_result = {"path": None}
def on_setup_complete(path) -> None:
logger.debug(f"Received setup_complete signal with path: {path}")
setup_result["path"] = path
# Connect to the setup_complete signal
dialog.setup_complete.connect(on_setup_complete)
# Show the dialog
result = dialog.exec()
logger.debug(f"Dialog result: {result} (Accepted={QDialog.DialogCode.Accepted})")
if result == QDialog.DialogCode.Accepted:
# First check if we received a path from the signal
if setup_result["path"]:
path_obj = Path(setup_result["path"]) if isinstance(setup_result["path"], str) else setup_result["path"]
if path_obj.exists():
logger.debug(f"Using path from signal: {setup_result['path']}")
return str(setup_result["path"])
# Get the expected path for verification as fallback
expected_path = YTDLP_APP_BIN_PATH
logger.debug(f"Expected yt-dlp path: {expected_path}")
# Verify the path exists after dialog is accepted
if expected_path.exists():
logger.debug(f"yt-dlp successfully found at expected path: {expected_path}")
return str(expected_path)
else:
logger.debug(f"Expected path does not exist, trying alternate detection")
# Try to use the get_yt_dlp_path function to find yt-dlp elsewhere
yt_dlp_path = get_yt_dlp_path()
logger.debug(f"Alternate detection result: {yt_dlp_path}")
if yt_dlp_path != "yt-dlp":
path_obj = Path(yt_dlp_path) if isinstance(yt_dlp_path, str) else yt_dlp_path
if path_obj.exists():
logger.debug(f"yt-dlp found at alternate location: {yt_dlp_path}")
return str(yt_dlp_path)
# Something went wrong, show an error message
logger.debug(f"Setup failed, showing error dialog")
if parent_widget:
error_dialog = QMessageBox(parent_widget)
error_dialog.setIcon(QMessageBox.Icon.Warning)
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(
"""
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;
}
"""
)
error_dialog.exec()
logger.warning(f"yt-dlp setup failed, path does not exist: {expected_path}")
else:
logger.debug("User cancelled the setup dialog")
# 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
+5
View File
@@ -0,0 +1,5 @@
"""
GUI modules for YTSage.
This package contains all user interface components and related functionality.
"""
+467
View File
@@ -0,0 +1,467 @@
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:
if "Private video" in result.stderr or "Sign in" in result.stderr:
logger.error(f"yt-dlp failed (private video): {result.stderr}")
self.analysis_error.emit(_("errors.private_video"))
else:
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, generic_mode=self.generic_mode_enabled)
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 = []
from ..utils.ytsage_config_manager import ConfigManager
default_sub = ConfigManager.get("default_subtitle_language")
if default_sub:
if isinstance(default_sub, str):
default_sub_list = [s.strip() for s in default_sub.split(",")]
else:
default_sub_list = []
for lang in default_sub_list:
if lang in self.available_subtitles:
self.selected_subtitles.append(f"{lang} - Manual")
elif lang in self.available_automatic_subtitles:
self.selected_subtitles.append(f"{lang} - Auto-generated")
count = len(self.selected_subtitles)
try:
self.signals.selected_subs_label_text.emit(_("subtitle_selection.count_selected", count=count))
self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0)
self.subtitle_select_btn.style().unpolish(self.subtitle_select_btn)
self.subtitle_select_btn.style().polish(self.subtitle_select_btn)
except Exception:
pass
# 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
count = len(self.selected_subtitles)
if count > 0:
self.signals.selected_subs_label_text.emit(_("subtitle_selection.count_selected", count=count))
else:
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
+61
View File
@@ -0,0 +1,61 @@
"""
Dialog modules for YTSage GUI.
This package contains all dialog classes organized by functionality:
- ytsage_dialogs_base: Base utility dialogs
- ytsage_dialogs_settings: Settings configuration dialogs
- ytsage_dialogs_update: Update-related dialogs and threads
- ytsage_dialogs_ffmpeg: FFmpeg installation dialogs
- ytsage_dialogs_selection: Subtitle and playlist selection dialogs
- ytsage_dialogs_custom: Custom functionality dialogs
"""
# Re-export all dialog classes for backward compatibility
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 .ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog
from .ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog
from .ytsage_dialogs_updater import UpdaterTabWidget
__all__ = [
# Base dialogs
"LogWindow",
"AboutDialog",
# Settings dialogs
"DownloadSettingsDialog",
"AutoUpdateSettingsDialog",
# Update dialogs and threads
"VersionCheckThread",
"UpdateThread",
"YTDLPUpdateDialog",
"AutoUpdateThread",
# FFmpeg dialogs
"FFmpegInstallThread",
"FFmpegCheckDialog",
# Selection dialogs
"SubtitleSelectionDialog",
"PlaylistSelectionDialog",
"SponsorBlockCategoryDialog",
# Custom functionality dialogs
"CustomOptionsDialog",
"TimeRangeDialog",
# Updater widget
"UpdaterTabWidget",
# History dialog
"HistoryDialog",
]
@@ -0,0 +1,640 @@
"""
Base dialogs for YTSage application.
Contains basic utility dialogs like LogWindow and AboutDialog.
"""
from datetime import datetime
from PySide6.QtCore import Qt, QThread, QTimer, Signal, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QSizePolicy,
QTextEdit,
QVBoxLayout,
QWidget,
)
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 ...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(_("dialogs.ytdlp_log_title"))
self.setMinimumSize(700, 500)
layout = QVBoxLayout(self)
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setStyleSheet(
"""
QTextEdit {
background-color: #2b2b2b;
color: #ffffff;
font-family: Consolas, monospace;
font-size: 12px;
border: 2px solid #3d3d3d;
border-radius: 4px;
}
"""
)
layout.addWidget(self.log_text)
def append_log(self, message) -> None:
self.log_text.append(message)
# Auto-scroll to bottom
scrollbar = self.log_text.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
class AboutDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._parent = parent # Store parent to access version etc.
self.setWindowTitle(_("about.title"))
self.setMinimumSize(460, 420) # Slightly increased to accommodate paths
self.resize(460, 440) # Slightly increased initial size
self.setMaximumSize(500, 480) # Reasonable maximum size
# Set window flags to make dialog independent of parent movement
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint)
layout = QVBoxLayout(self)
layout.setSpacing(15) # Reduced spacing
layout.setContentsMargins(20, 20, 20, 20) # Reduced margins
layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetMinAndMaxSize)
# App Information Section
app_info_widget = self._create_app_info_section()
layout.addWidget(app_info_widget)
# Separator - subtle and compact
separator = QWidget()
separator.setFixedHeight(1)
separator.setStyleSheet("background-color: #2a2a2a; margin: 8px 20px;")
layout.addWidget(separator)
# System Information Section
system_info_widget = self._create_system_info_section()
layout.addWidget(system_info_widget)
# Close Button - compact positioning
layout.addSpacing(10) # Reduced space before button
button_layout = QHBoxLayout()
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
button_box.accepted.connect(self.accept)
# Center the button
button_layout.addStretch()
button_layout.addWidget(button_box)
button_layout.addStretch()
layout.addLayout(button_layout)
# Apply overall styling - improved consistency
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #cccccc;
}
QPushButton {
padding: 10px 30px;
background-color: #c90000;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 12px;
min-width: 80px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
QGroupBox {
font-weight: bold;
border: 1px solid #333333;
border-radius: 8px;
margin-top: 12px;
padding-top: 12px;
background-color: #15181b;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #ffffff;
font-size: 14px;
}
"""
)
def _create_app_info_section(self) -> QWidget:
"""Create the application information section - compact version"""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(6) # Reduced spacing
# Title and Version - more compact
title_label = QLabel(
"<span style='color: #c90000; font-size: 28px; font-weight: 300; letter-spacing: 2px;'>YTSage</span>"
)
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
version_label = QLabel(
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>"
f"{_('about.version', version=getattr(self._parent, 'version', APP_VERSION))}</span>"
)
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(version_label)
# Description - more compact
description_label = QLabel(_("about.description"))
description_label.setWordWrap(True)
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
layout.addWidget(description_label)
# Author and Links - compact single line
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=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=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()
info_container.addLayout(info_layout)
info_container.addStretch()
layout.addLayout(info_container)
return widget
def _create_system_info_section(self) -> QWidget:
"""Create the system information section with compact design"""
# Create main container with compact styling
container = QWidget()
container.setStyleSheet(
"""
QWidget {
border: 1px solid #333333;
border-radius: 8px;
background-color: #15181b;
margin-top: 5px;
}
"""
)
main_layout = QVBoxLayout(container)
main_layout.setSpacing(8) # Compact spacing
main_layout.setContentsMargins(15, 10, 15, 10)
# Create header with title and refresh button on same line
header_layout = QHBoxLayout()
header_layout.setContentsMargins(0, 0, 0, 5)
# System Information title
title_label = QLabel(_("about.system_info"))
title_label.setStyleSheet(
"""
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: bold;
padding: 0px;
margin: 0px;
}
"""
)
header_layout.addWidget(title_label)
# 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)
self.refresh_btn.setStyleSheet(
"""
QPushButton {
padding: 0px;
background-color: transparent;
border: none;
color: #cccccc;
font-size: 10px;
font-weight: normal;
margin: 0px;
}
QPushButton:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
QPushButton:pressed {
color: #c90000;
background-color: rgba(201, 0, 0, 0.1);
}
"""
)
self.refresh_btn.clicked.connect(self.refresh_version_info)
header_layout.addWidget(self.refresh_btn)
main_layout.addLayout(header_layout)
# Compact status grid layout
self.status_container = QVBoxLayout()
self.status_container.setSpacing(6) # Tight spacing
self.status_container.setContentsMargins(0, 0, 0, 0)
main_layout.addLayout(self.status_container)
# Show loading message initially
self._show_loading_message()
# Populate system information asynchronously
QTimer.singleShot(100, self.update_system_info)
return container
def _show_loading_message(self) -> None:
"""Show a compact loading message while system information is being gathered."""
# 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: #888888;
font-size: 11px;
padding: 10px;
}
"""
)
self.status_container.addWidget(loading_label)
def _create_status_item(self, icon, name, status_text, version_text, path_text=None, cache_status="") -> QWidget:
"""Create a compact status item widget"""
item_widget = QWidget()
# Adjust height based on whether we have path info
item_height = 50 if path_text else 35
item_widget.setMaximumHeight(item_height)
item_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# Main layout
item_layout = QVBoxLayout(item_widget)
item_layout.setContentsMargins(8, 4, 8, 4)
item_layout.setSpacing(2)
# First row: Icon, name, status, version
first_row = QHBoxLayout()
first_row.setSpacing(8)
# Icon and name - compact
name_label = QLabel(f"{icon} <b>{name}</b>")
name_label.setStyleSheet("font-size: 12px; color: #ffffff; font-weight: bold;")
name_label.setMinimumWidth(80)
first_row.addWidget(name_label)
# Status - compact
status_label = QLabel(status_text)
status_label.setStyleSheet("font-size: 11px; font-weight: bold;")
status_label.setMinimumWidth(70)
first_row.addWidget(status_label)
# Version info - improved readability
version_info = version_text
if cache_status:
version_info += cache_status
version_label = QLabel(version_info)
version_label.setStyleSheet("font-size: 11px; color: #cccccc;") # Increased from 10px
version_label.setWordWrap(False)
first_row.addWidget(version_label)
# Add stretch to push everything left
first_row.addStretch()
item_layout.addLayout(first_row)
# Second row: Path (if provided)
if path_text:
path_label = QLabel(f"📁 {path_text}")
path_label.setStyleSheet("font-size: 10px; color: #aaaaaa; margin-left: 12px;") # Increased from 9px, better color
path_label.setWordWrap(False)
# Truncate very long paths
if len(str(path_text)) > 60:
truncated_path = "..." + str(path_text)[-57:]
path_label.setText(f"📁 {truncated_path}")
item_layout.addWidget(path_label)
# Subtle background with minimal border
item_widget.setStyleSheet(
"""
QWidget {
background-color: rgba(45, 45, 45, 0.3);
border: 1px solid #2a2a2a;
border-radius: 4px;
margin: 1px;
}
QWidget:hover {
background-color: rgba(60, 60, 60, 0.4);
}
"""
)
return item_widget
def update_system_info(self) -> None:
"""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()
if child:
child.deleteLater()
# yt-dlp Status - compact version with path
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 = info['ytdlp_version']
# Get yt-dlp path
ytdlp_path = info['ytdlp_path']
ytdlp_path_text = ytdlp_path if ytdlp_path and ytdlp_path != "yt-dlp" else None
# Simplified cache status
last_check = info['ytdlp_last_check']
cache_status = ""
if last_check > 0:
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
ytdlp_item = self._create_status_item(
"🎥",
"yt-dlp",
ytdlp_status_text,
ytdlp_version + cache_status,
ytdlp_path_text,
)
self.status_container.addWidget(ytdlp_item)
# FFmpeg Status - compact version with path
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 = info['ffmpeg_version']
# Get FFmpeg path
ffmpeg_path_text = info['ffmpeg_path']
# Simplified cache status for FFmpeg
last_check = info['ffmpeg_last_check']
cache_status = ""
if last_check > 0 and ffmpeg_found:
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
ffmpeg_item = self._create_status_item(
"🎬",
"FFmpeg",
ffmpeg_status_text,
ffmpeg_version + cache_status,
ffmpeg_path_text,
)
self.status_container.addWidget(ffmpeg_item)
# Deno Status - compact version with path (only show path if in app bin directory)
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 = info['deno_version']
# Get Deno path - only show if in app bin directory
deno_path_text = None
if deno_found:
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 ...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
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 + 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'))
self.refresh_btn.setEnabled(False)
# Perform refresh in a separate thread to avoid blocking UI
class RefreshThread(QThread):
finished = Signal(bool)
def run(self):
success = refresh_version_cache(force=True)
self.finished.emit(success)
self.refresh_thread = RefreshThread()
self.refresh_thread.finished.connect(self.on_refresh_finished)
self.refresh_thread.start()
def on_refresh_finished(self, success) -> None:
"""Handle refresh completion."""
self.refresh_btn.setText(_('about.refresh'))
self.refresh_btn.setEnabled(True)
if success:
self.update_system_info()
else:
# Show error message with proper styling
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle(_('about.refresh_failed'))
msg_box.setText(_('about.refresh_failed_message'))
msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet(
"""
QMessageBox {
background-color: #15181b;
color: #ffffff;
}
QMessageBox QLabel {
color: #ffffff;
}
QMessageBox QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #a50000;
}
"""
)
msg_box.exec()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,195 @@
"""
FFmpeg installation dialogs for YTSage application.
Contains dialogs and threads for checking and installing FFmpeg.
"""
import webbrowser
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
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):
finished = Signal(bool)
progress = Signal(str)
def run(self) -> None:
# Use a callback to capture progress instead of stdout redirection
def progress_callback(msg: str):
self.progress.emit(msg)
# Install FFmpeg with progress callback
success = auto_install_ffmpeg(progress_callback=progress_callback)
self.finished.emit(success)
class FFmpegCheckDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_("ffmpeg.installation_title"))
self.setMinimumWidth(500)
self.setMinimumHeight(280)
self.resize(500, 300)
# Set the window icon to match the main app
if parent and parent.windowIcon():
self.setWindowIcon(parent.windowIcon())
else:
# Try to load the icon directly if parent not available
# icon_path logic moved to src\utils\ytsage_constants.py
if ICON_PATH.exists():
self.setWindowIcon(QIcon(str(ICON_PATH)))
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Header with title and improved spacing
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(_("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)
layout.addWidget(self.message_label)
# Progress label with improved styling
self.progress_label = QLabel("")
self.progress_label.setWordWrap(True)
self.progress_label.setMinimumHeight(80)
self.progress_label.setMaximumHeight(120)
self.progress_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.progress_label.setStyleSheet(
"""
QLabel {
background-color: #1d1e22;
color: #cccccc;
border: 1px solid #3d3d3d;
border-radius: 6px;
padding: 12px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.4;
}
"""
)
self.progress_label.hide()
layout.addWidget(self.progress_label)
# Add stretch to push buttons to bottom
layout.addStretch()
# Buttons container - simple approach that should work
button_layout = QHBoxLayout()
button_layout.setSpacing(12)
# Install button
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(_("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(_("buttons.close"))
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.close_btn)
layout.addLayout(button_layout)
# Style the dialog to match app theme
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #cccccc;
}
QPushButton {
padding: 10px 20px;
background-color: #c90000;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-height: 20px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
QPushButton:disabled {
background-color: #666666;
color: #999999;
}
"""
)
# Initialize installation thread
self.install_thread = None
self.progress_messages = [] # Store progress messages
def start_installation(self) -> None:
self.install_btn.setEnabled(False)
self.manual_btn.setEnabled(False)
self.close_btn.setEnabled(False)
# Check if FFmpeg is already installed
if check_ffmpeg_installed():
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(_("ffmpeg.installing"))
self.progress_messages = [] # Clear previous messages
self.progress_label.show()
self.install_thread = FFmpegInstallThread()
self.install_thread.finished.connect(self.installation_finished)
self.install_thread.progress.connect(self.update_progress)
self.install_thread.start()
def update_progress(self, message) -> None:
# Keep only the last 5 messages to avoid overflow
self.progress_messages.append(message)
if len(self.progress_messages) > 5:
self.progress_messages.pop(0)
# Display the messages
self.progress_label.setText("\n".join(self.progress_messages))
def installation_finished(self, success) -> None:
if success:
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_failed"))
self.progress_label.setText(_("ffmpeg.try_manual"))
self.install_btn.setEnabled(True)
self.manual_btn.setEnabled(True)
self.close_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") or 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)
@@ -0,0 +1,730 @@
"""
Selection dialogs for YTSage application.
Contains dialogs for selecting subtitles, playlist videos, and SponsorBlock categories.
"""
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox,
QDialog,
QDialogButtonBox,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from ...utils.ytsage_localization import _
class SubtitleSelectionDialog(QDialog):
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_("dialogs.select_subtitles"))
self.setMinimumWidth(400)
self.setMinimumHeight(300)
self.available_manual = available_manual
self.available_auto = available_auto
self.previously_selected = set(previously_selected) # Use a set for quick lookups
self.selected_subtitles = list(previously_selected) # Initialize with previous selection
layout = QVBoxLayout(self)
layout.setSpacing(10)
# Filter input
self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText(_("dialogs.filter_languages_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;
}
"""
)
layout.addWidget(self.filter_input)
# Scroll Area for the list
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area
layout.addWidget(scroll_area)
# Container widget for list items (needed for scroll area)
self.list_container = QWidget()
self.list_layout = QVBoxLayout(self.list_container)
self.list_layout.setContentsMargins(0, 0, 0, 0)
self.list_layout.setSpacing(2) # Compact spacing
self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top
scroll_area.setWidget(self.list_container)
# Populate the list initially
self.populate_list()
# OK and Cancel buttons
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
# Style the buttons
for button in button_box.buttons():
button.setStyleSheet(
"""
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 15px; /* Adjust padding */
min-height: 30px; /* Ensure consistent height */
color: white;
}
QPushButton:hover {
background-color: #444444;
}
QPushButton:pressed {
background-color: #555555;
}
"""
)
# Style the OK button specifically if needed
if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
button.setStyleSheet(
button.styleSheet()
+ "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }"
)
layout.addWidget(button_box)
def populate_list(self, filter_text="") -> None:
# Clear existing checkboxes from layout
while self.list_layout.count():
item = self.list_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
filter_text = filter_text.lower()
combined_subs = {}
# Add manual subs
for lang_code, sub_info in self.available_manual.items():
if not filter_text or filter_text in lang_code.lower():
combined_subs[lang_code] = f"{lang_code} - Manual"
# Add auto subs (only if no manual exists and matches filter)
for lang_code, sub_info in self.available_auto.items():
if lang_code not in combined_subs: # Don't overwrite manual
if not filter_text or filter_text in lang_code.lower():
combined_subs[lang_code] = f"{lang_code} - Auto-generated"
if not combined_subs:
matching_text = _("dialogs.matching") if filter_text else ""
no_subs_label = QLabel(_("dialogs.no_subtitles_available") + (f" {matching_text} '{filter_text}'" if filter_text else ""))
no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
self.list_layout.addWidget(no_subs_label)
return
# Sort by language code
sorted_lang_codes = sorted(combined_subs.keys())
for lang_code in sorted_lang_codes:
item_text = combined_subs[lang_code]
checkbox = QCheckBox(item_text)
checkbox.setProperty("subtitle_id", item_text) # Store the identifier
checkbox.setChecked(item_text in self.previously_selected) # Check if previously selected
checkbox.stateChanged.connect(self.update_selection)
checkbox.setStyleSheet(
"""
QCheckBox {
color: #ffffff;
padding: 5px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px; /* Square checkboxes */
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #2b2b2b;
}
QCheckBox::indicator:checked {
border: 2px solid #ff0000;
background: #ff0000;
}
"""
)
self.list_layout.addWidget(checkbox)
self.list_layout.addStretch() # Pushes items up if list is short
def filter_list(self) -> None:
self.populate_list(self.filter_input.text())
def update_selection(self, state) -> None:
sender = self.sender()
subtitle_id = sender.property("subtitle_id")
if state == Qt.CheckState.Checked.value:
if subtitle_id not in self.previously_selected:
self.previously_selected.add(subtitle_id)
else:
if subtitle_id in self.previously_selected:
self.previously_selected.remove(subtitle_id)
def get_selected_subtitles(self) -> list:
# Return the final set as a list
return list(self.previously_selected)
def accept(self) -> None:
# Update the final list before closing
self.selected_subtitles = self.get_selected_subtitles()
super().accept()
class PlaylistSelectionDialog(QDialog):
def __init__(self, playlist_entries, previously_selected_string, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_("playlist.select_videos_title"))
self.setMinimumWidth(500)
self.setMinimumHeight(400) # Allow more vertical space
self.playlist_entries = playlist_entries
self.checkboxes = []
# 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"))
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
select_all_btn.clicked.connect(self._select_all)
deselect_all_btn.clicked.connect(self._deselect_all)
# Style the buttons to match the subtitle dialog
select_all_btn.setStyleSheet(
"""
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 15px;
min-height: 30px;
color: white;
}
QPushButton:hover {
background-color: #444444;
}
QPushButton:pressed {
background-color: #555555;
}
"""
)
deselect_all_btn.setStyleSheet(select_all_btn.styleSheet())
button_layout.addWidget(select_all_btn)
button_layout.addWidget(deselect_all_btn)
button_layout.addStretch()
main_layout.addLayout(button_layout)
# Scrollable area for checkboxes
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area
scroll_widget = QWidget()
self.list_layout = QVBoxLayout(scroll_widget) # Layout for checkboxes
self.list_layout.setContentsMargins(0, 0, 0, 0)
self.list_layout.setSpacing(2) # Compact spacing
self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top
scroll_area.setWidget(scroll_widget)
main_layout.addWidget(scroll_area)
# Populate checkboxes
self._populate_list(previously_selected_string)
# Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
# Style the buttons to match subtitle dialog
for button in button_box.buttons():
button.setStyleSheet(
"""
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 15px;
min-height: 30px;
color: white;
}
QPushButton:hover {
background-color: #444444;
}
QPushButton:pressed {
background-color: #555555;
}
"""
)
# Style the OK button specifically if needed
if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
button.setStyleSheet(
button.styleSheet()
+ "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }"
)
main_layout.addWidget(button_box)
# Apply styling to match subtitle dialog
self.setStyleSheet(
"""
QDialog { background-color: #15181b; }
QCheckBox {
color: #ffffff;
padding: 5px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #2b2b2b;
}
QCheckBox::indicator:checked {
border: 2px solid #ff0000;
background: #ff0000;
}
QWidget { background-color: #15181b; }
"""
)
def _parse_selection_string(self, selection_string) -> set:
"""Parses a yt-dlp playlist selection string (e.g., '1-3,5,7-9') into a set of 1-based indices."""
selected_indices = set()
if not selection_string:
# If no previous selection, assume all are selected initially
return set(range(1, len(self.playlist_entries) + 1))
parts = selection_string.split(",")
for part in parts:
part = part.strip()
if "-" in part:
try:
start, end = map(int, part.split("-"))
if start <= end:
selected_indices.update(range(start, end + 1))
except ValueError:
pass # Ignore invalid ranges
else:
try:
selected_indices.add(int(part))
except ValueError:
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)
# Clear existing checkboxes if any (e.g., if repopulating)
while self.list_layout.count():
child = self.list_layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
self.checkboxes.clear()
for index, entry in enumerate(self.playlist_entries):
if not entry:
continue # Skip None entries if yt-dlp returns them
video_index = index + 1 # yt-dlp uses 1-based indexing
title = entry.get("title", f"Video {video_index}")
# 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 {
color: #ffffff;
padding: 5px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #2b2b2b;
}
QCheckBox::indicator:checked {
border: 2px solid #ff0000;
background: #ff0000;
}
"""
)
self.list_layout.addWidget(checkbox)
self.checkboxes.append(checkbox)
self.list_layout.addStretch() # Push checkboxes to the top
def _select_all(self) -> None:
for checkbox in self.checkboxes:
checkbox.setChecked(True)
def _deselect_all(self) -> None:
for checkbox in self.checkboxes:
checkbox.setChecked(False)
def _condense_indices(self, indices: list[int]) -> str:
"""Condenses a list of 1-based indices into a yt-dlp selection string."""
if not indices:
return ""
# Remove duplicates and sort in one step
indices = sorted(set(indices))
ranges = []
start = end = indices[0]
for num in indices[1:]:
if num == end + 1:
end = num
else:
ranges.append(f"{start}-{end}" if start != end else str(start))
start = end = num
# Append the last range
ranges.append(f"{start}-{end}" if start != end else str(start))
return ",".join(ranges)
def get_selected_items_string(self) -> str | None:
"""Returns the selection string based on checked boxes."""
selected_indices = [cb.property("video_index") for cb in self.checkboxes if cb.isChecked()]
# Check if all items are selected
if len(selected_indices) == len(self.playlist_entries):
return None # yt-dlp default is all items, so return None or empty string
return self._condense_indices(selected_indices)
class SponsorBlockCategoryDialog(QDialog):
"""Dialog for selecting SponsorBlock categories to remove from videos."""
# Default SponsorBlock categories with descriptions
SPONSORBLOCK_CATEGORIES = {
"sponsor": {
"name_key": "sponsorblock.sponsor",
"description_key": "sponsorblock.sponsor_desc",
"default": True,
},
"selfpromo": {
"name_key": "sponsorblock.selfpromo",
"description_key": "sponsorblock.selfpromo_desc",
"default": True,
},
"interaction": {
"name_key": "sponsorblock.interaction",
"description_key": "sponsorblock.interaction_desc",
"default": True,
},
"intro": {
"name_key": "sponsorblock.intro",
"description_key": "sponsorblock.intro_desc",
"default": False,
},
"outro": {
"name_key": "sponsorblock.outro",
"description_key": "sponsorblock.outro_desc",
"default": False,
},
"preview": {
"name_key": "sponsorblock.preview",
"description_key": "sponsorblock.preview_desc",
"default": False,
},
"music_offtopic": {
"name_key": "sponsorblock.music_offtopic",
"description_key": "sponsorblock.music_offtopic_desc",
"default": False,
},
"filler": {
"name_key": "sponsorblock.filler",
"description_key": "sponsorblock.filler_desc",
"default": False,
},
}
def __init__(self, previously_selected=None, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_("dialogs.sponsorblock_categories"))
self.setMinimumWidth(500)
self.setMinimumHeight(400)
# Set the window icon to match the main app
if parent:
self.setWindowIcon(parent.windowIcon())
self.previously_selected = set(previously_selected) if previously_selected else set()
self.checkboxes = {}
self.init_ui()
self.apply_styling()
def init_ui(self) -> None:
layout = QVBoxLayout(self)
# Title and description
title_label = QLabel(_("dialogs.sponsorblock_categories"))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
layout.addWidget(title_label)
desc_label = QLabel(_("dialogs.sponsorblock_description"))
desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
layout.addWidget(desc_label)
# Scroll area for categories
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("QScrollArea { border: none; }")
scroll_widget = QWidget()
scroll_layout = QVBoxLayout(scroll_widget)
scroll_layout.setContentsMargins(10, 0, 10, 0)
scroll_layout.setSpacing(8)
# Add category checkboxes
for category_id, category_info in self.SPONSORBLOCK_CATEGORIES.items():
# Create a container widget for each category
category_widget = QWidget()
category_layout = QVBoxLayout(category_widget)
category_layout.setContentsMargins(0, 0, 0, 0)
category_layout.setSpacing(2)
# Create checkbox with localized name
checkbox = QCheckBox(_(category_info["name_key"]))
checkbox.setProperty("category_id", category_id)
# Determine if this category should be checked
if self.previously_selected:
# Use previously selected categories
is_checked = category_id in self.previously_selected
else:
# Use default values for first time
is_checked = category_info["default"]
checkbox.setChecked(is_checked)
checkbox.setStyleSheet(
"""
QCheckBox {
color: #ffffff;
padding: 4px;
spacing: 10px;
font-weight: bold;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #2b2b2b;
}
QCheckBox::indicator:checked {
border: 2px solid #ff0000;
background: #ff0000;
}
"""
)
# Create description label with localized text
desc_label = QLabel(_(category_info["description_key"]))
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
desc_label.setWordWrap(True)
category_layout.addWidget(checkbox)
category_layout.addWidget(desc_label)
self.checkboxes[category_id] = checkbox
scroll_layout.addWidget(category_widget)
scroll_layout.addStretch()
scroll_area.setWidget(scroll_widget)
layout.addWidget(scroll_area)
# Quick selection buttons
button_layout = QHBoxLayout()
select_defaults_btn = QPushButton(_("buttons.select_defaults"))
select_defaults_btn.clicked.connect(self.select_defaults)
select_defaults_btn.setStyleSheet(self._get_button_style())
select_all_btn = QPushButton(_("buttons.select_all"))
select_all_btn.clicked.connect(self.select_all)
select_all_btn.setStyleSheet(self._get_button_style())
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
deselect_all_btn.clicked.connect(self.deselect_all)
deselect_all_btn.setStyleSheet(self._get_button_style())
button_layout.addWidget(select_defaults_btn)
button_layout.addWidget(select_all_btn)
button_layout.addWidget(deselect_all_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
# Dialog buttons
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
# Style the dialog buttons
for button in button_box.buttons():
button.setStyleSheet(self._get_button_style())
if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
button.setStyleSheet(
button.styleSheet()
+ "QPushButton { background-color: #ff0000; border-color: #cc0000; } "
+ "QPushButton:hover { background-color: #cc0000; }"
)
layout.addWidget(button_box)
def _get_button_style(self) -> str:
"""Returns the standard button style for this dialog."""
return """
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 15px;
min-height: 30px;
color: white;
}
QPushButton:hover {
background-color: #444444;
}
QPushButton:pressed {
background-color: #555555;
}
"""
def apply_styling(self) -> None:
"""Apply the dialog styling to match the rest of the application."""
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QWidget {
background-color: #15181b;
}
"""
)
def select_defaults(self) -> None:
"""Select only the default categories."""
for category_id, checkbox in self.checkboxes.items():
default_value = self.SPONSORBLOCK_CATEGORIES[category_id]["default"]
checkbox.setChecked(default_value)
def select_all(self) -> None:
"""Select all categories."""
for checkbox in self.checkboxes.values():
checkbox.setChecked(True)
def deselect_all(self) -> None:
"""Deselect all categories."""
for checkbox in self.checkboxes.values():
checkbox.setChecked(False)
def get_selected_categories(self) -> list:
"""Returns a list of selected category IDs."""
selected = []
for category_id, checkbox in self.checkboxes.items():
if checkbox.isChecked():
selected.append(category_id)
return selected
def get_selected_categories_string(self) -> str:
"""Returns a comma-separated string of selected categories for yt-dlp."""
selected = self.get_selected_categories()
return ",".join(selected) if selected else ""
@@ -0,0 +1,989 @@
"""
Settings-related dialogs for YTSage application.
Contains dialogs for configuring download settings.
"""
import threading
import time
from datetime import datetime
import requests
from packaging import version as version_parser
from PySide6.QtCore import Qt, QTimer, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QButtonGroup,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QRadioButton,
QVBoxLayout,
QWidget,
)
from ..ytsage_smooth_tab_widget import SmoothTabWidget
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):
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_("settings.title"))
self.setMinimumWidth(550)
self.setMinimumHeight(400)
self.current_path = current_path
self.current_limit = current_limit if current_limit is not None else ""
self.current_unit_index = current_unit_index
# Apply main app styling
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
}
QFrame#tabContent {
border: 1px solid #3d3d3d;
background-color: #15181b;
}
QTabBar::tab {
background-color: #1d1e22;
color: #ffffff;
padding: 8px 12px;
border: 1px solid #3d3d3d;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background-color: #c90000;
}
QTabBar::tab:hover:!selected {
background-color: #2a2d36;
}
QWidget {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QGroupBox {
border: 1px solid #3d3d3d;
border-radius: 4px;
margin-top: 1.5ex;
color: #ffffff;
padding: 10px;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 5px;
color: #ffffff;
}
QLineEdit {
padding: 8px;
border: 2px solid #1b2021;
border-radius: 4px;
background-color: #1b2021;
color: #ffffff;
selection-background-color: #c90000;
selection-color: #ffffff;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-height: 20px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
QPushButton:disabled {
background-color: #666666;
color: #999999;
}
QCheckBox {
spacing: 5px;
color: #ffffff;
}
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;
}
QRadioButton {
spacing: 5px;
color: #ffffff;
}
QRadioButton::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QRadioButton::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QRadioButton::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
QComboBox {
padding: 8px;
border: 2px solid #1b2021;
border-radius: 4px;
background-color: #1b2021;
color: #ffffff;
min-width: 150px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
border: none;
width: 12px;
height: 12px;
}
QComboBox QAbstractItemView {
background-color: #1d1e22;
color: #ffffff;
border: 1px solid #3d3d3d;
selection-background-color: #c90000;
}
"""
)
layout = QVBoxLayout(self)
# Create tab widget
self.tab_widget = SmoothTabWidget()
layout.addWidget(self.tab_widget)
# === General Tab ===
general_tab = QWidget()
general_layout = QVBoxLayout(general_tab)
# --- Download Path Section ---
path_group_box = QGroupBox(_("settings.download_path"))
path_layout = QVBoxLayout()
self.path_display = QLabel(str(self.current_path))
self.path_display.setWordWrap(True)
self.path_display.setStyleSheet(
"QLabel { color: #ffffff; padding: 5px; border: 1px solid #1b2021; border-radius: 4px; background-color: #1b2021; }"
)
path_layout.addWidget(self.path_display)
browse_button = QPushButton(_("settings.browse"))
browse_button.clicked.connect(self.browse_new_path)
path_layout.addWidget(browse_button)
path_group_box.setLayout(path_layout)
general_layout.addWidget(path_group_box)
# --- Speed Limit Section ---
speed_group_box = QGroupBox(_("settings.speed_limit"))
speed_layout = QHBoxLayout()
self.speed_limit_input = QLineEdit(str(self.current_limit))
self.speed_limit_input.setPlaceholderText(_("settings.speed_limit_placeholder"))
speed_layout.addWidget(self.speed_limit_input)
self.speed_limit_unit = QComboBox()
self.speed_limit_unit.addItems(["KB/s", "MB/s"])
self.speed_limit_unit.setCurrentIndex(self.current_unit_index)
speed_layout.addWidget(self.speed_limit_unit)
speed_group_box.setLayout(speed_layout)
general_layout.addWidget(speed_group_box)
# --- Connections Section ---
connections_group_box = QGroupBox(_("settings.concurrent_fragments", default="Concurrent Connections"))
connections_layout = QVBoxLayout()
self.connections_enabled = ConfigManager.get("concurrent_fragments") or 1
connections_spin_layout = QHBoxLayout()
self.connections_input = QComboBox()
self.connections_input.addItems([str(i) for i in range(1, 21)])
self.connections_input.setCurrentText(str(self.connections_enabled))
connections_spin_layout.addWidget(self.connections_input)
connections_spin_layout.addStretch()
connections_layout.addLayout(connections_spin_layout)
connections_help_label = QLabel(_("settings.concurrent_fragments_help", default="Number of connections per download. Higher values bypass throttling but may cause temporary blocks if set too high. Default: 1."))
connections_help_label.setWordWrap(True)
connections_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
connections_layout.addWidget(connections_help_label)
connections_group_box.setLayout(connections_layout)
general_layout.addWidget(connections_group_box)
# --- Generic Mode Section ---
generic_mode_group_box = QGroupBox(_("settings.generic_mode"))
generic_mode_layout = QVBoxLayout()
generic_val = ConfigManager.get("generic_mode")
self.generic_mode_enabled = generic_val if generic_val is not None else True
self.generic_mode_checkbox = QCheckBox(_("settings.enable_generic_mode"))
self.generic_mode_checkbox.setChecked(self.generic_mode_enabled)
generic_mode_layout.addWidget(self.generic_mode_checkbox)
generic_mode_help_label = QLabel(_("settings.generic_mode_help"))
generic_mode_help_label.setWordWrap(True)
generic_mode_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
generic_mode_layout.addWidget(generic_mode_help_label)
generic_mode_group_box.setLayout(generic_mode_layout)
general_layout.addWidget(generic_mode_group_box)
general_layout.addStretch()
# === Format Tab ===
format_tab = QWidget()
format_layout = QVBoxLayout(format_tab)
# --- Output Format Settings Section ---
output_format_group_box = QGroupBox(_("settings.output_format_settings"))
output_format_layout = QVBoxLayout()
# Load current format settings from ConfigManager
self.force_format_enabled = ConfigManager.get("force_output_format") or False
self.preferred_format_value = ConfigManager.get("preferred_output_format") or "mp4"
# Enable/Disable force output format checkbox
self.force_format_checkbox = QCheckBox(_("settings.force_output_format"))
self.force_format_checkbox.setChecked(self.force_format_enabled)
output_format_layout.addWidget(self.force_format_checkbox)
# Format selection layout
format_select_layout = QHBoxLayout()
format_label = QLabel(_("settings.preferred_format"))
format_label.setStyleSheet("color: #ffffff; margin-top: 5px;")
format_select_layout.addWidget(format_label)
self.format_combo = QComboBox()
self.format_combo.addItems([
_("settings.format_mp4"),
_("settings.format_webm"),
_("settings.format_mkv")
])
# Set current selection based on saved format
format_index_map = {"mp4": 0, "webm": 1, "mkv": 2}
self.format_combo.setCurrentIndex(format_index_map.get(self.preferred_format_value, 0))
format_select_layout.addWidget(self.format_combo)
format_select_layout.addStretch()
output_format_layout.addLayout(format_select_layout)
# Help text
help_label = QLabel(_("settings.force_format_help"))
help_label.setWordWrap(True)
help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
output_format_layout.addWidget(help_label)
output_format_group_box.setLayout(output_format_layout)
format_layout.addWidget(output_format_group_box)
# --- Audio Format Settings Section (for audio-only downloads) ---
audio_format_group_box = QGroupBox(_("settings.audio_format_settings"))
audio_format_layout = QVBoxLayout()
# Load current audio format settings from ConfigManager
self.force_audio_format_enabled = ConfigManager.get("force_audio_format") or False
self.preferred_audio_format_value = ConfigManager.get("preferred_audio_format") or "best"
self.audio_normalization_enabled = ConfigManager.get("audio_normalization") or False
# Enable/Disable force audio format checkbox
self.force_audio_format_checkbox = QCheckBox(_("settings.force_audio_format"))
self.force_audio_format_checkbox.setChecked(self.force_audio_format_enabled)
audio_format_layout.addWidget(self.force_audio_format_checkbox)
# Enable/Disable audio normalization checkbox
self.audio_normalization_checkbox = QCheckBox(_("settings.audio_normalization", default="Audio Normalization"))
self.audio_normalization_checkbox.setChecked(self.audio_normalization_enabled)
audio_format_layout.addWidget(self.audio_normalization_checkbox)
# Audio normalization help text
audio_norm_help_label = QLabel(_("settings.audio_normalization_help", default="When enabled, audio will be normalized using EBU R128 standard."))
audio_norm_help_label.setWordWrap(True)
audio_norm_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
audio_format_layout.addWidget(audio_norm_help_label)
# Audio format selection layout
audio_format_select_layout = QHBoxLayout()
audio_format_label = QLabel(_("settings.preferred_audio_format"))
audio_format_label.setStyleSheet("color: #ffffff; margin-top: 5px;")
audio_format_select_layout.addWidget(audio_format_label)
self.audio_format_combo = QComboBox()
self.audio_format_combo.addItems([
_("settings.audio_format_best"),
_("settings.audio_format_aac"),
_("settings.audio_format_mp3"),
_("settings.audio_format_flac"),
_("settings.audio_format_wav"),
_("settings.audio_format_opus"),
_("settings.audio_format_m4a"),
_("settings.audio_format_vorbis")
])
# Set current selection based on saved format
audio_format_index_map = {"best": 0, "aac": 1, "mp3": 2, "flac": 3, "wav": 4, "opus": 5, "m4a": 6, "vorbis": 7}
self.audio_format_combo.setCurrentIndex(audio_format_index_map.get(self.preferred_audio_format_value, 0))
audio_format_select_layout.addWidget(self.audio_format_combo)
audio_format_select_layout.addStretch()
audio_format_layout.addLayout(audio_format_select_layout)
# 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: 11px;")
audio_format_layout.addWidget(audio_help_label)
# Connect signals
self.audio_normalization_checkbox.stateChanged.connect(self._on_audio_normalization_toggled)
self.force_audio_format_checkbox.stateChanged.connect(self._on_force_audio_format_toggled)
audio_format_group_box.setLayout(audio_format_layout)
format_layout.addWidget(audio_format_group_box)
# --- Default Quality and Subtitles Section ---
defaults_group_box = QGroupBox(_("settings.defaults_settings", default="Default Selection Settings"))
defaults_layout = QVBoxLayout()
# Default Video Quality
vid_qual_layout = QHBoxLayout()
vid_qual_label = QLabel(_("settings.default_video_quality", default="Default Video Resolution (Height):"))
vid_qual_label.setStyleSheet("color: #ffffff; margin-top: 5px;")
self.default_vid_qual_input = QLineEdit(str(ConfigManager.get("default_video_quality") or ""))
self.default_vid_qual_input.setPlaceholderText("e.g. 1080 or 720")
vid_qual_layout.addWidget(vid_qual_label)
vid_qual_layout.addWidget(self.default_vid_qual_input)
defaults_layout.addLayout(vid_qual_layout)
# Default Subtitles
sub_layout = QHBoxLayout()
sub_label = QLabel(_("settings.default_subtitle_language", default="Default Subtitle Language(s):"))
sub_label.setStyleSheet("color: #ffffff; margin-top: 5px;")
self.default_sub_input = QLineEdit(ConfigManager.get("default_subtitle_language") or "")
self.default_sub_input.setPlaceholderText("e.g. en, es")
sub_layout.addWidget(sub_label)
sub_layout.addWidget(self.default_sub_input)
defaults_layout.addLayout(sub_layout)
defaults_help = QLabel(_("settings.defaults_help", default="Set your preferred video height and subtitle languages (comma-separated). They will be auto-selected if available."))
defaults_help.setWordWrap(True)
defaults_help.setStyleSheet("color: #cccccc; margin: 5px; font-size: 11px;")
defaults_layout.addWidget(defaults_help)
defaults_group_box.setLayout(defaults_layout)
format_layout.addWidget(defaults_group_box)
format_layout.addStretch()
# === File Tab ===
file_tab = QWidget()
file_layout = QVBoxLayout(file_tab)
# --- 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_[%(id)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_[%(id)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_[%(id)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)
file_layout.addWidget(filename_format_group_box)
file_layout.addStretch()
# Add tabs to tab widget
self.tab_widget.addTab(general_tab, _("settings.tab_general", default="General"))
self.tab_widget.addTab(format_tab, _("settings.tab_format", default="Format"))
self.tab_widget.addTab(file_tab, _("settings.tab_file", default="File"))
# Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
# Style the buttons to look identical to Custom Options dialog
for btn in [ok_button, cancel_button]:
btn.setMinimumHeight(35)
btn.setMinimumWidth(80)
btn.setStyleSheet(
"""
QPushButton {
padding: 8px 20px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
"""
)
layout.addWidget(button_box)
def _on_audio_normalization_toggled(self, state: int) -> None:
"""Handle logic when audio normalization is toggled."""
if state == Qt.CheckState.Checked.value:
# Normalization requires re-encoding, so we must force an audio format
self.force_audio_format_checkbox.setChecked(True)
# If 'Best (No conversion)' is selected, change it to MP3 to ensure re-encoding
if self.audio_format_combo.currentIndex() == 0:
self.audio_format_combo.setCurrentIndex(2) # Index 2 is typically MP3
def _on_force_audio_format_toggled(self, state: int) -> None:
"""Handle logic when force audio format is toggled."""
if state == Qt.CheckState.Unchecked.value:
# If re-encoding is disabled, normalization cannot happen
self.audio_normalization_checkbox.setChecked(False)
def browse_new_path(self) -> None:
new_path = QFileDialog.getExistingDirectory(self, _("dialogs.select_folder"), str(self.current_path))
if new_path:
self.current_path = new_path
self.path_display.setText(self.current_path)
def get_selected_path(self) -> str:
"""Returns the confirmed path after the dialog is accepted."""
return self.current_path
def get_selected_speed_limit(self) -> str | None:
"""Returns the entered speed limit value (as string or None)."""
limit_str = self.speed_limit_input.text().strip()
if not limit_str:
return None
try:
float(limit_str) # Check if convertible to float
return limit_str
except ValueError:
logger.info("Invalid speed limit input in dialog")
return None
def get_selected_unit_index(self) -> int:
"""Returns the index of the selected speed limit unit."""
return self.speed_limit_unit.currentIndex()
def get_force_format_enabled(self) -> bool:
"""Returns whether force output format is enabled."""
return self.force_format_checkbox.isChecked()
def get_generic_mode_enabled(self) -> bool:
"""Returns whether generic mode is enabled."""
return self.generic_mode_checkbox.isChecked()
def get_concurrent_fragments(self) -> int:
"""Returns the number of concurrent fragments."""
try:
return int(self.connections_input.currentText())
except ValueError:
return 1
def get_preferred_format(self) -> str:
"""Returns the selected preferred format (lowercase)."""
format_map = {0: "mp4", 1: "webm", 2: "mkv"}
return format_map.get(self.format_combo.currentIndex(), "mp4")
def get_force_audio_format_enabled(self) -> bool:
"""Returns whether force audio format is enabled."""
return self.force_audio_format_checkbox.isChecked()
def get_preferred_audio_format(self) -> str:
"""Returns the selected preferred audio format (lowercase)."""
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_audio_normalization_enabled(self) -> bool:
"""Returns whether audio normalization is enabled."""
return self.audio_normalization_checkbox.isChecked()
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)
msg_box.setIcon(icon)
msg_box.setWindowTitle(title)
msg_box.setText(text)
msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet(
"""
QMessageBox {
background-color: #15181b;
color: #ffffff;
}
QMessageBox QLabel {
color: #ffffff;
}
QMessageBox QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #a50000;
}
QMessageBox QPushButton:pressed {
background-color: #800000;
}
"""
)
return msg_box
def accept(self) -> None:
"""Override accept to save format settings."""
try:
ConfigManager.set("generic_mode", self.get_generic_mode_enabled())
ConfigManager.set("concurrent_fragments", self.get_concurrent_fragments())
# Save output format settings
force_format = self.get_force_format_enabled()
preferred_format = self.get_preferred_format()
ConfigManager.set("force_output_format", force_format)
ConfigManager.set("preferred_output_format", preferred_format)
# Save audio format settings
force_audio_format = self.get_force_audio_format_enabled()
preferred_audio_format = self.get_preferred_audio_format()
audio_normalization = self.get_audio_normalization_enabled()
ConfigManager.set("force_audio_format", force_audio_format)
ConfigManager.set("preferred_audio_format", preferred_audio_format)
ConfigManager.set("audio_normalization", audio_normalization)
# Save defaults
default_vid = self.default_vid_qual_input.text().strip()
ConfigManager.set("default_video_quality", default_vid if default_vid else None)
default_sub = self.default_sub_input.text().strip()
ConfigManager.set("default_subtitle_language", default_sub if default_sub else None)
# 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"),
_("settings.settings_saved_message"),
)
except Exception as e:
QMessageBox.critical(self, _("settings.error_title"), _("settings.error_saving_settings", error=str(e)))
# Call the parent accept method to close the dialog
super().accept()
class AutoUpdateSettingsDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_("settings.auto_update_title"))
self.setMinimumWidth(400)
self.setMinimumHeight(300)
# Set the window icon to match the main app
if parent:
self.setWindowIcon(parent.windowIcon())
self.init_ui()
self.load_current_settings()
self.apply_styling()
def init_ui(self) -> None:
layout = QVBoxLayout(self)
# Title
title_label = QLabel(f"<h2>{_("settings.auto_update_header")}</h2>")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
# Description
desc_label = QLabel(_("settings.auto_update_description"))
desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
layout.addWidget(desc_label)
# Enable/Disable auto-update
self.enable_checkbox = QCheckBox(_("settings.enable_auto_updates"))
self.enable_checkbox.setChecked(True) # Default enabled
self.enable_checkbox.toggled.connect(self.on_enable_toggled)
layout.addWidget(self.enable_checkbox)
# Frequency options
frequency_group = QGroupBox(_("settings.update_frequency_group"))
frequency_layout = QVBoxLayout()
self.frequency_group = QButtonGroup(self)
self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
self.daily_radio.setChecked(True) # Default to daily
self.frequency_group.addButton(self.startup_radio, 0)
self.frequency_group.addButton(self.daily_radio, 1)
self.frequency_group.addButton(self.weekly_radio, 2)
frequency_layout.addWidget(self.startup_radio)
frequency_layout.addWidget(self.daily_radio)
frequency_layout.addWidget(self.weekly_radio)
frequency_group.setLayout(frequency_layout)
layout.addWidget(frequency_group)
# Current status
status_group = QGroupBox(_("settings.current_status"))
status_layout = QVBoxLayout()
self.current_version_label = QLabel(_("settings.current_version_label"))
self.last_check_label = QLabel(_("settings.last_check_label"))
self.next_check_label = QLabel(_("settings.next_check_label"))
status_layout.addWidget(self.current_version_label)
status_layout.addWidget(self.last_check_label)
status_layout.addWidget(self.next_check_label)
status_group.setLayout(status_layout)
layout.addWidget(status_group)
# Manual check button
self.manual_check_btn = QPushButton(_("settings.manual_check_button"))
self.manual_check_btn.clicked.connect(self.manual_check)
layout.addWidget(self.manual_check_btn)
# Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton(_("settings.save_settings"))
self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def apply_styling(self) -> None:
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QGroupBox {
color: #ffffff;
border: 2px solid #1b2021;
border-radius: 4px;
margin-top: 10px;
padding-top: 10px;
font-weight: bold;
background-color: #15181b;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #ffffff;
}
QCheckBox, QRadioButton {
color: #ffffff;
spacing: 5px;
margin: 5px;
}
QCheckBox::indicator, QRadioButton::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QCheckBox::indicator:unchecked, QRadioButton::indicator:unchecked {
border: 2px solid #666666;
background: #15181b;
}
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
margin: 5px;
min-width: 100px;
min-height: 20px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
QPushButton:disabled {
background-color: #666666;
color: #999999;
}
"""
)
def load_current_settings(self) -> None:
"""Load current auto-update settings from config."""
try:
settings = get_auto_update_settings()
# Set checkbox
self.enable_checkbox.setChecked(settings["enabled"])
# Set frequency
frequency = settings["frequency"]
if frequency == "startup":
self.startup_radio.setChecked(True)
elif frequency == "weekly":
self.weekly_radio.setChecked(True)
else: # daily
self.daily_radio.setChecked(True)
# Update status labels
current_version = get_ytdlp_version()
self.current_version_label.setText(_("auto_update.current_version", version=current_version))
last_check = settings["last_check"]
if last_check > 0:
last_check_time = datetime.fromtimestamp(last_check).strftime("%Y-%m-%d %H:%M:%S")
self.last_check_label.setText(_("auto_update.last_check", time=last_check_time))
else:
self.last_check_label.setText(_("auto_update.last_check_never"))
# Calculate next check time
self.update_next_check_label()
# Update UI state
self.on_enable_toggled(settings["enabled"])
except Exception as e:
logger.exception(f"Error loading auto-update settings: {e}")
def update_next_check_label(self) -> None:
"""Update the next check label based on current settings."""
try:
if not self.enable_checkbox.isChecked():
self.next_check_label.setText(_("auto_update.next_check_disabled"))
return
settings = get_auto_update_settings()
last_check = settings["last_check"]
frequency = self.get_selected_frequency()
if last_check == 0:
self.next_check_label.setText(_("auto_update.next_check_startup"))
return
next_check_time = last_check
if frequency == "startup":
next_check_time += 3600 # 1 hour
elif frequency == "daily":
next_check_time += 86400 # 24 hours
elif frequency == "weekly":
next_check_time += 604800 # 7 days
current_time = time.time()
if next_check_time <= current_time:
self.next_check_label.setText(_("auto_update.next_check_overdue"))
else:
next_check_datetime = datetime.fromtimestamp(next_check_time)
self.next_check_label.setText(_("auto_update.next_check", time=next_check_datetime.strftime('%Y-%m-%d %H:%M:%S')))
except Exception as e:
self.next_check_label.setText(_("auto_update.next_check_error"))
logger.exception(f"Error calculating next check time: {e}")
def on_enable_toggled(self, enabled) -> None:
"""Handle enable/disable checkbox toggle."""
# Enable/disable frequency options
for i in range(self.frequency_group.buttons().__len__()):
self.frequency_group.button(i).setEnabled(enabled)
self.update_next_check_label()
def get_selected_frequency(self) -> str:
"""Get the selected frequency setting."""
if self.startup_radio.isChecked():
return "startup"
elif self.weekly_radio.isChecked():
return "weekly"
else:
return "daily"
def manual_check(self) -> None:
"""Perform a manual update check."""
self.manual_check_btn.setEnabled(False)
self.manual_check_btn.setText(_("auto_update.checking"))
# Force an immediate update check
def check_in_thread() -> None:
try:
result = check_and_update_ytdlp_auto()
# Update UI in main thread
QTimer.singleShot(0, lambda: self.manual_check_finished(result))
except Exception as e:
logger.exception(f"Error during manual check: {e}")
QTimer.singleShot(0, lambda: self.manual_check_finished(False))
# Run in separate thread to avoid blocking UI
threading.Thread(target=check_in_thread, daemon=True).start()
def _create_styled_message_box(self, icon, title, text) -> QMessageBox:
"""Create a styled QMessageBox that matches the app theme."""
msg_box = QMessageBox(self)
msg_box.setIcon(icon)
msg_box.setWindowTitle(title)
msg_box.setText(text)
msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet(
"""
QMessageBox {
background-color: #15181b;
color: #ffffff;
}
QMessageBox QLabel {
color: #ffffff;
}
QMessageBox QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #a50000;
}
QMessageBox QPushButton:pressed {
background-color: #800000;
}
"""
)
return msg_box
def manual_check_finished(self, success) -> None:
"""Handle completion of manual update check."""
self.manual_check_btn.setEnabled(True)
self.manual_check_btn.setText(_("auto_update.check_now"))
if success:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Update Check",
"✅ Update check completed successfully!\nCheck the console for details.",
)
msg_box.exec()
else:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Update Check",
"❌ Update check failed.\nCheck the console for error details.",
)
msg_box.exec()
# Refresh the current settings display
self.load_current_settings()
def save_settings(self) -> None:
"""Save the auto-update settings."""
try:
enabled = self.enable_checkbox.isChecked()
frequency = self.get_selected_frequency()
if update_auto_update_settings(enabled, frequency):
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
_("settings.settings_saved_title"),
_("settings.settings_saved_successfully"),
)
msg_box.exec()
self.accept()
else:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
_("settings.error_title"),
_("settings.failed_save_settings"),
)
msg_box.exec()
except Exception as e:
logger.exception(f"Error saving auto-update settings: {e}")
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, _("settings.error_title"), _("settings", "error_saving", error=str(e)))
msg_box.exec()
@@ -0,0 +1,485 @@
"""
Update-related dialogs and threads for YTSage application.
Contains dialogs and background threads for checking and performing yt-dlp binary updates.
Note: This module only handles binary updates. Python package updates have been removed.
"""
import os
import subprocess
import time
from pathlib import Path
import requests
from packaging import version
from PySide6.QtCore import Qt, QThread, QTimer, Signal
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout
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 ...utils.ytsage_localization import _
from ...utils.ytsage_logger import logger
class VersionCheckThread(QThread):
finished = Signal(str, str, str) # current_version, latest_version, error_message
def run(self) -> None:
current_version = ""
latest_version = ""
error_message = ""
try:
# Get the yt-dlp executable path
yt_dlp_path = get_yt_dlp_path()
# Get current version with timeout
try:
result = subprocess.run(
[yt_dlp_path, "--version"],
capture_output=True,
text=True,
timeout=30, # 30 second timeout
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if result.returncode == 0:
current_version = result.stdout.strip()
else:
error_message = "yt-dlp binary not accessible."
self.finished.emit(current_version, latest_version, error_message)
return
except subprocess.TimeoutExpired:
error_message = "yt-dlp version check timed out."
self.finished.emit(current_version, latest_version, error_message)
return
except Exception as e:
error_message = f"yt-dlp not found or accessible: {e}"
self.finished.emit(current_version, latest_version, error_message)
return
# Get latest version from PyPI (yt-dlp releases are also published to PyPI)
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
response.raise_for_status()
latest_version = response.json()["info"]["version"]
# Clean up version strings
current_version = current_version.replace("_", ".")
latest_version = latest_version.replace("_", ".")
except requests.RequestException as e:
error_message = f"Network error checking PyPI: {e}"
except Exception as e:
error_message = f"Error checking version: {e}"
self.finished.emit(current_version, latest_version, error_message)
class UpdateThread(QThread):
update_status = Signal(str) # For status messages
update_progress = Signal(int) # For progress percentage (0-100)
update_finished = Signal(bool, str) # success (bool), message/error (str)
def run(self) -> None:
error_message = ""
success = False
try:
self.update_status.emit(_('update.checking_current'))
self.update_progress.emit(10)
# Get the yt-dlp path
try:
yt_dlp_path = get_yt_dlp_path()
self.update_status.emit(_('update.found_at', path=yt_dlp_path))
except Exception as e:
self.update_status.emit(_('update.error_getting_path', error=e))
self.update_finished.emit(False, _('update.error_getting_path', error=e))
return
self.update_progress.emit(20)
# Update the binary (no more pip-based updates)
self.update_status.emit(_('update.updating_binary'))
success = self._update_binary(yt_dlp_path)
if success:
self.update_progress.emit(100)
error_message = _('update.update_success')
else:
error_message = _('update.update_failed')
except requests.RequestException as e:
error_message = _('update.network_error', error=e)
self.update_status.emit(error_message)
success = False
except Exception as e:
error_message = _('update.general_error', error=e)
self.update_status.emit(error_message)
success = False
self.update_finished.emit(success, error_message)
def _update_binary(self, yt_dlp_path: Path) -> bool:
"""Update yt-dlp binary using its built-in updater (same logic as AutoUpdateThread)."""
try:
logger.info("UpdateThread: Checking for yt-dlp updates...")
result = subprocess.run(
[yt_dlp_path, "-U"],
capture_output=True,
text=True,
timeout=60,
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if result.returncode == 0:
# Make executable on Unix systems
if OS_NAME != "Windows":
os.chmod(yt_dlp_path, 0o755)
logger.info("UpdateThread: yt-dlp update completed successfully.")
if result.stdout:
logger.debug(f"yt-dlp output: {result.stdout.strip()}")
self.update_status.emit(_('update.binary_updated'))
self.update_progress.emit(95)
return True
else:
logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}")
self.update_status.emit(_('update.update_failed_stderr', error=result.stderr.strip()))
return False
except subprocess.TimeoutExpired:
logger.error("UpdateThread: yt-dlp update timed out.")
self.update_status.emit(_('update.update_timeout'))
return False
except Exception as e:
logger.exception(f"UpdateThread: Unexpected error during update: {e}")
self.update_status.emit(_('update.unexpected_error', error=e))
return False
class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle(_('update.title'))
self.setMinimumWidth(450)
self.setMinimumHeight(200)
self._closing = False # Flag to track if dialog is closing
layout = QVBoxLayout(self)
# Status label
self.status_label = QLabel(_('update.checking'))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setWordWrap(True)
self.status_label.setMinimumHeight(60)
layout.addWidget(self.status_label)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.hide() # Hide initially
layout.addWidget(self.progress_bar)
# Buttons
button_layout = QHBoxLayout()
self.update_btn = QPushButton(_('buttons.update'))
self.update_btn.clicked.connect(self.perform_update)
self.update_btn.setEnabled(False)
self.close_btn = QPushButton(_('buttons.close'))
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.update_btn)
button_layout.addWidget(self.close_btn)
layout.addLayout(button_layout)
# Style
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
}
QLabel {
color: #ffffff;
font-size: 12px;
padding: 10px;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 100px;
}
QPushButton:disabled {
background-color: #666666;
}
QPushButton:hover {
background-color: #a50000;
}
QProgressBar {
border: 2px solid #1d1e22;
border-radius: 6px;
text-align: center;
color: white;
background-color: #1d1e22;
height: 30px;
font-weight: bold;
font-size: 12px;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #e60000, stop: 0.5 #ff3333, stop: 1 #c90000);
border-radius: 4px;
margin: 1px;
}
"""
)
# Start version check in background
self.check_version()
def check_version(self) -> None:
self.status_label.setText(_('update.checking'))
self.update_btn.setEnabled(False)
self.version_check_thread = VersionCheckThread()
self.version_check_thread.finished.connect(self.on_version_check_finished)
self.version_check_thread.start()
def on_version_check_finished(self, current_version, latest_version, error_message) -> None:
# Check if dialog is closing to avoid unnecessary updates
if hasattr(self, "_closing") and self._closing:
return
if error_message:
self.status_label.setText(error_message)
self.update_btn.setEnabled(False)
return
if not current_version or not latest_version:
self.status_label.setText(_('update.could_not_determine'))
self.update_btn.setEnabled(False)
return
try:
# Compare versions
current_ver = version.parse(current_version)
latest_ver = version.parse(latest_version)
if current_ver < latest_ver:
self.status_label.setText(
_('update.update_available', current=current_version, latest=latest_version)
)
self.update_btn.setEnabled(True)
else:
self.status_label.setText(_('update.already_latest', version=current_version))
self.update_btn.setEnabled(False)
except version.InvalidVersion:
# If version parsing fails, do a simple string comparison
if current_version != latest_version:
self.status_label.setText(
_('update.update_available_failed', current=current_version, latest=latest_version)
)
self.update_btn.setEnabled(True)
else:
self.status_label.setText(_('update.up_to_date', version=current_version))
self.update_btn.setEnabled(False)
except Exception as e:
self.status_label.setText(_('update.error_comparing', error=e))
self.update_btn.setEnabled(False)
def perform_update(self) -> None:
# Immediate visual feedback
self.update_btn.setEnabled(False)
self.close_btn.setEnabled(False)
self.update_btn.setText(_('update.updating'))
self.status_label.setText(_('update.initializing'))
# Show progress bar immediately
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.show()
# Start the update thread
self._start_update_thread()
def _start_update_thread(self) -> None:
"""Start the actual update thread."""
# Create and start the update thread
self.update_thread = UpdateThread()
self.update_thread.update_status.connect(self.on_update_status)
self.update_thread.update_progress.connect(self.on_update_progress)
self.update_thread.update_finished.connect(self.on_update_finished)
self.update_thread.start()
def on_update_status(self, message) -> None:
"""Slot to receive status messages from UpdateThread."""
if not (hasattr(self, "_closing") and self._closing):
self.status_label.setText(message)
def on_update_progress(self, progress) -> None:
"""Slot to receive progress updates from UpdateThread."""
if not (hasattr(self, "_closing") and self._closing):
self.progress_bar.setValue(progress)
def on_update_finished(self, success, message) -> None:
"""Slot called when the UpdateThread finishes."""
# Check if dialog is closing to avoid unnecessary updates
if hasattr(self, "_closing") and self._closing:
return
self.progress_bar.setValue(100)
self.status_label.setText(message)
self.close_btn.setEnabled(True)
self.update_btn.setText(_('buttons.update')) # Reset button text
if success:
# Show success briefly then auto-check version
QTimer.singleShot(2000, self.check_version) # Wait 2 seconds then refresh
else:
# Re-enable update button on failure after a short delay
QTimer.singleShot(
3000,
lambda: (self.update_btn.setEnabled(True) if not (hasattr(self, "_closing") and self._closing) else None),
)
def closeEvent(self, event) -> None:
"""Ensure threads are terminated if the dialog is closed prematurely."""
# Set a flag to indicate dialog is closing
self._closing = True
if hasattr(self, "version_check_thread") and self.version_check_thread.isRunning():
self.version_check_thread.quit()
if not self.version_check_thread.wait(3000): # Wait up to 3 seconds
self.version_check_thread.terminate()
if hasattr(self, "update_thread") and self.update_thread.isRunning():
self.update_thread.quit()
if not self.update_thread.wait(5000): # Wait up to 5 seconds for update to finish
self.update_thread.terminate()
super().closeEvent(event)
class AutoUpdateThread(QThread):
"""Thread for performing automatic background updates without UI feedback."""
update_finished = Signal(bool, str) # success (bool), message (str)
def run(self) -> None:
"""Perform automatic yt-dlp update check and update if needed."""
try:
logger.info("AutoUpdateThread: Performing automatic yt-dlp update check...")
# Get current version
current_version = get_ytdlp_version()
if "Error" in current_version:
logger.warning("AutoUpdateThread: Could not determine current yt-dlp version, skipping auto-update")
self.update_finished.emit(False, "Could not determine current yt-dlp version")
return
# Get latest version from PyPI
try:
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
response.raise_for_status()
latest_version = response.json()["info"]["version"]
# Clean up version strings
current_version = current_version.replace("_", ".")
latest_version = latest_version.replace("_", ".")
logger.info(f"AutoUpdateThread: Current yt-dlp version: {current_version}")
logger.info(f"AutoUpdateThread: Latest yt-dlp version: {latest_version}")
# Compare versions
if version.parse(latest_version) > version.parse(current_version):
logger.info(f"AutoUpdateThread: Auto-updating yt-dlp from {current_version} to {latest_version}...")
# Perform the update
success = self._perform_update()
if success:
logger.info("AutoUpdateThread: Auto-update completed successfully!")
# Update the last check timestamp
ConfigManager.set("last_update_check", time.time())
self.update_finished.emit(
True,
f"Successfully updated yt-dlp from {current_version} to {latest_version}",
)
else:
logger.warning("AutoUpdateThread: Auto-update failed")
self.update_finished.emit(False, "Auto-update failed")
else:
logger.info("AutoUpdateThread: yt-dlp is already up to date")
# Still update the timestamp even if no update was needed
ConfigManager.set("last_update_check", time.time())
self.update_finished.emit(
True,
f"yt-dlp is already up to date (version {current_version})",
)
except requests.RequestException as e:
logger.warning(f"AutoUpdateThread: Network error during auto-update check: {e}")
self.update_finished.emit(False, f"Network error: {e}")
except Exception as e:
logger.exception(f"AutoUpdateThread: Error during auto-update check: {e}", exc_info=True)
self.update_finished.emit(False, f"Update check error: {e}")
except Exception as e:
logger.critical(f"AutoUpdateThread: Critical error in auto-update: {e}", exc_info=True)
self.update_finished.emit(False, f"Critical error: {e}")
def _perform_update(self) -> bool:
"""Perform the actual binary update."""
try:
# Get the yt-dlp path
yt_dlp_path = get_yt_dlp_path()
# Always update the binary (no more pip-based updates)
logger.info("AutoUpdateThread: Updating yt-dlp binary...")
return self._update_binary(yt_dlp_path)
except Exception as e:
logger.exception(f"AutoUpdateThread: Error in _perform_update: {e}")
return False
def _update_binary(self, yt_dlp_path: Path) -> bool:
"""Update yt-dlp binary using its built-in updater."""
try:
logger.info("AutoUpdateThread: Checking for yt-dlp updates...")
result = subprocess.run(
[yt_dlp_path, "-U"],
capture_output=True,
text=True,
timeout=60,
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if result.returncode == 0:
# Make executable on Unix systems
if OS_NAME != "Windows":
os.chmod(yt_dlp_path, 0o755)
logger.info("AutoUpdateThread: yt-dlp update completed successfully.")
if result.stdout:
logger.debug(f"yt-dlp output: {result.stdout.strip()}")
return True
else:
logger.error(f"AutoUpdateThread: yt-dlp update failed. {result.stderr.strip()}")
return False
except subprocess.TimeoutExpired:
logger.error("AutoUpdateThread: yt-dlp update timed out.")
return False
except Exception as e:
logger.exception(f"AutoUpdateThread: Unexpected error during update: {e}")
return False
File diff suppressed because it is too large Load Diff
+610
View File
@@ -0,0 +1,610 @@
from typing import TYPE_CHECKING, cast
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 ..utils.ytsage_localization import _
if TYPE_CHECKING:
from .ytsage_gui_main import YTSageApp
class FormatSignals(QObject):
format_update = Signal(list)
class FormatTableMixin:
def _calculate_column_width(self, label: str, min_width: int, padding: int) -> int:
"""Calculate responsive column width based on header text length."""
self = cast("YTSageApp", self)
font_metrics = QFontMetrics(self.format_table.horizontalHeader().font())
text_width = font_metrics.horizontalAdvance(label)
return max(text_width + padding, min_width)
def _get_audio_codec_display(self, format_info: dict) -> str:
"""
Generate a display string for audio codec information.
Highlights EAC3/AC3 and other surround sound formats with channel info.
"""
acodec = format_info.get("acodec")
if not acodec or acodec == "none":
return "N/A"
channels = format_info.get("audio_channels")
abr = format_info.get("abr")
# Build codec string
codec_str = str(acodec)
# Add channel info for surround sound formats
if channels:
codec_str += f" ({channels}ch)"
# Add bitrate if available
if abr and isinstance(abr, (int, float)):
if abr >= 1000:
codec_str += f" {abr/1000:.1f}Mbps"
else:
codec_str += f" {int(abr)}kbps"
return codec_str
def _apply_column_widths(self, header_labels: list[str], is_playlist_mode: bool = False) -> None:
"""Apply responsive column widths to format table."""
self = cast("YTSageApp", self)
if is_playlist_mode:
# Playlist mode: 6 columns
configs = [
{"min_width": 70, "padding": 40}, # Select
{"min_width": 100, "padding": 30}, # Quality
{"min_width": 100, "padding": 30}, # Resolution
{"min_width": 60, "padding": 30}, # FPS
{"min_width": 60, "padding": 30}, # HDR
{"min_width": 100, "padding": 30}, # Audio
]
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
calculated_width = self._calculate_column_width(header_labels[0], configs[0]["min_width"], configs[0]["padding"])
self.format_table.setColumnWidth(0, calculated_width)
# Remaining columns stretch
for i in range(1, 6):
self.format_table.horizontalHeader().setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch)
else:
# Normal mode: 9 columns
configs = [
{"min_width": 70, "padding": 40, "stretch": False}, # Select - fixed
{"min_width": 100, "padding": 30, "stretch": True}, # Quality - stretch
{"min_width": 85, "padding": 30, "stretch": False}, # Extension - fixed
{"min_width": 100, "padding": 30, "stretch": True}, # Resolution - stretch
{"min_width": 90, "padding": 30, "stretch": True}, # File Size - stretch
{"min_width": 100, "padding": 30, "stretch": True}, # Codec - stretch
{"min_width": 100, "padding": 30, "stretch": True}, # Audio - stretch
{"min_width": 60, "padding": 30, "stretch": False}, # FPS - fixed
{"min_width": 60, "padding": 30, "stretch": False}, # HDR - fixed
]
# Apply column widths with mixed fixed and stretch modes
for col_index, (label, config) in enumerate(zip(header_labels, configs)):
calculated_width = self._calculate_column_width(label, config["min_width"], config["padding"])
if config["stretch"]:
# Stretchable columns for flexible content
self.format_table.horizontalHeader().setSectionResizeMode(col_index, QHeaderView.ResizeMode.Stretch)
else:
# Fixed columns for consistent sizing
self.format_table.horizontalHeader().setSectionResizeMode(col_index, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(col_index, calculated_width)
def setup_format_table(self) -> QTableWidget:
self = cast("YTSageApp", self) # for autocompletion and type inference.
self.format_signals = FormatSignals()
# Format table with improved styling
self.format_table = QTableWidget()
self.format_table.setColumnCount(9)
# Get translated header labels
header_labels = [
_("formats.select"),
_("formats.quality"),
_("formats.extension"),
_("formats.resolution"),
_("formats.file_size"),
_("formats.codec"),
_("formats.audio"),
_("formats.fps"),
_("formats.hdr"),
]
self.format_table.setHorizontalHeaderLabels(header_labels)
# Enable alternating row colors
self.format_table.setAlternatingRowColors(True)
# Apply responsive column widths
self._apply_column_widths(header_labels, is_playlist_mode=False)
# Set vertical header (row numbers) visible to false
self.format_table.verticalHeader().setVisible(False)
# Set selection mode to no selection (since we're using checkboxes)
self.format_table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
# Disable editing to prevent the selection box on double-click
self.format_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.format_table.setStyleSheet(
"""
QTableWidget {
background-color: #1b2021;
border: 2px solid #1b2021;
border-radius: 4px;
gridline-color: #1b2021;
}
QTableWidget::item {
padding: 5px;
border-bottom: 1px solid #1b2021;
}
QTableWidget::item:selected {
background-color: transparent;
}
QHeaderView::section {
background-color: #15181b;
padding: 5px;
border: 1px solid #1b2021;
font-weight: bold;
color: white;
}
/* Style alternating rows with more contrast */
QTableWidget::item:alternate {
background-color: #212529;
}
QTableWidget::item {
background-color: #16191b;
}
QCheckBox::indicator {
width: 16px;
height: 16px;
border-radius: 8px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #15181b;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
}
QWidget {
background-color: transparent;
}
"""
)
# 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)
# Set minimum and maximum heights
self.format_table.setMinimumHeight(200)
# Connect the signal
self.format_signals.format_update.connect(self._update_format_table)
return self.format_table
def filter_formats(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
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()
# 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 str(f.get("format_note") or "").lower())
and f.get("acodec") != "none"
# Removed filesize requirement - many audio-only formats (including EAC3/AC3) may not have filesize in yt-dlp output
# Include all audio formats to detect EAC3/AC3 and other surround sound codecs
]
# Sort formats by quality
def get_quality(f):
if f.get("vcodec") != "none":
resolution = f.get("resolution", "0x0")
if resolution is None or not isinstance(resolution, str):
return 0
try:
res = resolution.split("x")[-1]
return int(res)
except (ValueError, IndexError):
return 0
else:
abr = f.get("abr") or 0
return abr if isinstance(abr, (int, float)) else 0
video_formats.sort(key=get_quality, reverse=True)
audio_formats.sort(key=get_quality, reverse=True)
# Combine: video first, then audio (maintains logical grouping)
all_filtered = [(f, "video") for f in video_formats] + [(f, "audio") for f in audio_formats]
# Build table with format type tracking
self.format_table.setVisible(False)
self._populate_format_table(all_filtered)
self._table_built = True
from ..utils.ytsage_config_manager import ConfigManager
default_video_quality = ConfigManager.get("default_video_quality")
# Auto-select the default or best format
if self.format_checkboxes:
found_default = False
if default_video_quality:
# Try to find a video format with the requested height (e.g. '1080')
target_height = str(default_video_quality)
# the formats are sorted by quality, so the first match is typically the best one for that height
for idx, (f, fmt_type) in enumerate(all_filtered):
if fmt_type == "video":
res = str(f.get("resolution", ""))
if res.endswith(f"x{target_height}") or target_height in res:
self.format_checkboxes[idx].setChecked(True)
self.handle_checkbox_click(self.format_checkboxes[idx])
found_default = True
break
if not found_default:
# Fallback to the first available format (which is the best quality since it's sorted)
self.format_checkboxes[0].setChecked(True)
self.handle_checkbox_click(self.format_checkboxes[0])
# 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.
is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue]
# Configure columns based on mode
if is_playlist_mode:
self.format_table.setColumnCount(6)
header_labels = [_("formats.select"), _("formats.quality"), _("formats.resolution"), _("formats.fps"), _("formats.hdr"), _("formats.audio")]
self.format_table.setHorizontalHeaderLabels(header_labels)
# Configure column visibility and resizing for playlist mode
self.format_table.setColumnHidden(6, True)
self.format_table.setColumnHidden(7, True)
self.format_table.setColumnHidden(8, True)
# Apply responsive column widths for playlist mode
self._apply_column_widths(header_labels, is_playlist_mode=True)
else:
self.format_table.setColumnCount(9)
header_labels = [
_("formats.select"),
_("formats.quality"),
_("formats.extension"),
_("formats.resolution"),
_("formats.file_size"),
_("formats.codec"),
_("formats.audio"),
_("formats.fps"),
_("formats.hdr"),
]
self.format_table.setHorizontalHeaderLabels(header_labels)
self._apply_column_widths(header_labels, is_playlist_mode=False)
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)
# Create checkbox widget
checkbox = QCheckBox()
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)
# 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)
self.format_table.setCellWidget(row, 0, checkbox_container)
# Quality label with color coding
quality_label = self.get_quality_label(f)
if is_playlist_mode and f.get("vcodec") != "none":
quality_label = f"{quality_label}"
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", "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", "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)
# Resolution
resolution = f.get("resolution") or "N/A"
if not isinstance(resolution, str):
resolution = str(resolution)
if is_playlist_mode and f.get("vcodec") != "none" and resolution != "N/A":
resolution = f"{resolution}"
if is_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 and fps_value >= 1:
fps_text = f"{fps_value:.0f}fps"
else:
fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text)
if fps_value and fps_value >= 60:
fps_item.setForeground(QColor("#00ff00"))
elif fps_value and fps_value >= 30:
fps_item.setForeground(QColor("#ffaa00"))
elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555"))
else:
fps_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 3, fps_item)
# Column 4: HDR
if f.get("vcodec") == "none":
hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text)
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"))
else:
hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 4, hdr_item)
else:
# Extension for normal mode (column 2)
extension = str(f.get("ext") or "")
self.format_table.setItem(row, 2, QTableWidgetItem(extension.upper()))
# Audio Status column
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none"
acodec = f.get("acodec")
# Enhanced audio status with support for surround sound formats (EAC3/AC3)
if needs_audio:
audio_status = _("formats.will_merge_audio")
audio_color = QColor("#ffa500")
elif f.get("vcodec") != "none":
# Video format with audio
if acodec in ["ac3", "eac3"]:
# Highlight surround sound codecs
channels = f.get("audio_channels", "")
if channels:
audio_status = f"{_('formats.has_audio')} - {acodec.upper()} {channels}ch"
else:
audio_status = f"{_('formats.has_audio')} - {acodec.upper()}"
audio_color = QColor("#00ccff") # Cyan for surround sound
else:
audio_status = _("formats.has_audio")
audio_color = QColor("#00cc00")
else:
# Audio-only format
if acodec in ["ac3", "eac3"]:
# Highlight surround sound audio-only
channels = f.get("audio_channels", "")
if channels:
audio_status = f"{acodec.upper()} {channels}ch"
else:
audio_status = acodec.upper()
audio_color = QColor("#00ccff") # Cyan for surround sound
else:
audio_status = _("formats.audio_only")
audio_color = QColor("#cccccc")
audio_item = QTableWidgetItem(audio_status)
audio_item.setForeground(audio_color)
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
if not is_playlist_mode:
# Column 3: Resolution
self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
# Column 4: File Size
filesize = f"{f.get('filesize', 0) / 1024 / 1024:.2f} MB"
self.format_table.setItem(row, 4, QTableWidgetItem(filesize))
# Column 5: Codec
if f.get("vcodec") == "none":
# Audio-only format - display codec with channel info
codec = str(f.get("acodec") or "N/A")
# Add audio channel information if available (e.g., for 5.1 detection)
channels = f.get("audio_channels")
if channels:
codec += f" ({channels}ch)"
else:
# Video format - display video codec and audio codec if present
codec = str(f.get("vcodec") or "N/A")
if f.get("acodec") != "none":
acodec = str(f.get("acodec") or "N/A")
# Add audio channel info for video with audio
channels = f.get("audio_channels")
if channels:
acodec += f" ({channels}ch)"
codec += f" / {acodec}"
self.format_table.setItem(row, 5, QTableWidgetItem(codec))
# Column 7: FPS (Frame Rate)
fps_value = f.get("fps")
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)
if fps_value and fps_value >= 60:
fps_item.setForeground(QColor("#00ff00"))
elif fps_value and fps_value >= 30:
fps_item.setForeground(QColor("#ffaa00"))
elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555"))
else:
fps_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 7, fps_item)
# Column 8: HDR (Dynamic Range)
if f.get("vcodec") == "none":
hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text)
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"))
else:
hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text)
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.
for checkbox in self.format_checkboxes:
if checkbox != clicked_checkbox:
checkbox.setChecked(False)
def get_selected_format(self):
self = cast("YTSageApp", self) # for autocompletion and type inference.
for checkbox in self.format_checkboxes:
if checkbox.isChecked():
return {
"format_id": checkbox.format_id,
"is_audio_only": getattr(checkbox, "is_audio_only", False),
"has_audio": getattr(checkbox, "has_audio", False),
}
return None
def update_format_table(self, formats) -> None:
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:
"""Determine quality label based on format information"""
self = cast("YTSageApp", self) # for autocompletion and type inference.
if format_info.get("vcodec") == "none":
# Audio quality
abr = format_info.get("abr") or 0
if not isinstance(abr, (int, float)):
abr = 0
if abr >= 256:
return _("formats.best_audio")
elif abr >= 192:
return _("formats.high_audio")
elif abr >= 128:
return _("formats.medium_audio")
else:
return _("formats.low_audio")
else:
# Video quality
height = 0
resolution = format_info.get("resolution", "")
if resolution:
try:
parts = resolution.split("x")
if len(parts) == 2:
# Use the smaller dimension to correctly classify vertical videos
height = min(int(parts[0]), int(parts[1]))
except:
pass
if height >= 2160:
return _("formats.best_4k")
elif height >= 1440:
return _("formats.best_2k")
elif height >= 1080:
return _("formats.high_1080p")
elif height >= 720:
return _("formats.high_720p")
elif height >= 480:
return _("formats.medium_480p")
else:
return _("formats.low_quality")
File diff suppressed because it is too large Load Diff
+473
View File
@@ -0,0 +1,473 @@
import re
from datetime import datetime
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, cast
import requests
from PIL import Image
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from .ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py
SponsorBlockCategoryDialog,
SubtitleSelectionDialog,
)
from ..utils.ytsage_localization import _
from ..utils.ytsage_logger import logger
if TYPE_CHECKING:
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:
def setup_video_info_section(self) -> QHBoxLayout:
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Create a horizontal layout for thumbnail and video info
media_info_layout = QHBoxLayout()
media_info_layout.setSpacing(15)
# Left side container for thumbnail
thumbnail_container = QWidget()
thumbnail_container.setFixedWidth(320)
thumbnail_layout = QVBoxLayout(thumbnail_container)
thumbnail_layout.setContentsMargins(0, 0, 0, 0)
# Thumbnail on the left
self.thumbnail_label = QLabel()
self.thumbnail_label.setFixedSize(320, 180)
self.thumbnail_label.setStyleSheet("border: 2px solid #3d3d3d; border-radius: 4px;")
self.thumbnail_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
thumbnail_layout.addWidget(self.thumbnail_label)
thumbnail_layout.addStretch()
media_info_layout.addWidget(thumbnail_container)
# Video information on the right
video_info_layout = QVBoxLayout()
video_info_layout.setSpacing(2) # Reduce spacing between elements
video_info_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Title and info labels
self.title_label = QLabel()
self.title_label.setWordWrap(True)
self.title_label.setStyleSheet("font-size: 12px; font-weight: bold;")
# Add basic info labels
self.channel_label = QLabel()
self.views_label = QLabel()
self.date_label = QLabel()
self.duration_label = QLabel()
self.like_count_label = QLabel()
# Style the info labels
for label in [
self.channel_label,
self.views_label,
self.date_label,
self.duration_label,
self.like_count_label,
]:
label.setStyleSheet(
"""
QLabel {
color: #999999;
font-size: 11px;
padding: 0px;
}
"""
)
# Add labels to video info layout
video_info_layout.addWidget(self.title_label)
video_info_layout.addWidget(self.channel_label)
video_info_layout.addWidget(self.views_label)
video_info_layout.addWidget(self.like_count_label)
video_info_layout.addWidget(self.date_label)
video_info_layout.addWidget(self.duration_label)
# Add spacing before subtitle section
video_info_layout.addSpacing(10)
# --- Subtitle Section ---
subtitle_layout = QHBoxLayout()
subtitle_layout.setSpacing(10)
# Subtitle selection button
self.subtitle_select_btn = QPushButton(_("main_ui.select_subtitles")) # Renamed & changed text
self.subtitle_select_btn.setFixedHeight(30)
# self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed
self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog)
self.subtitle_select_btn.setStyleSheet(
"""
QPushButton {
background-color: #1d1e22;
border: 2px solid #1d1e22;
border-radius: 4px;
padding: 5px 10px; /* Adjusted padding */
}
QPushButton:hover { background-color: #2a2d36; }
/* Optional: Style differently if subtitles ARE selected */
QPushButton[subtitlesSelected="true"] {
border-color: #c90000; /* Indicate selection */
}
/* Style for disabled state */
QPushButton:disabled {
background-color: #3d3d3d;
color: #888888;
border-color: #3d3d3d;
}
"""
)
self.subtitle_select_btn.setProperty("subtitlesSelected", False) # Custom property for styling
subtitle_layout.addWidget(self.subtitle_select_btn)
# Label to show number of selected subtitles
self.selected_subs_label = QLabel(_("selection.none_selected"))
self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
subtitle_layout.addWidget(self.selected_subs_label)
# Add the subtitle layout to the main video info layout
video_info_layout.addLayout(subtitle_layout)
# --- End Subtitle Section ---
# Add small spacing between subtitle and sponsorblock sections
video_info_layout.addSpacing(4)
# --- SponsorBlock Section ---
sponsorblock_layout = QHBoxLayout()
self.sponsorblock_select_btn = QPushButton(_("main_ui.sponsorblock_categories"))
self.sponsorblock_select_btn.setFixedHeight(30)
self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog)
self.sponsorblock_select_btn.setStyleSheet(
"""
QPushButton {
background-color: #1d1e22;
border: 2px solid #1d1e22;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #2a2d36;
}
QPushButton[sponsorBlockSelected="true"] {
border-color: #c90000;
}
QPushButton:disabled {
background-color: #3d3d3d;
color: #888888;
border-color: #3d3d3d;
}
"""
)
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", False)
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", False)
sponsorblock_layout.addWidget(self.sponsorblock_select_btn)
# Label to show selection count
self.selected_sponsorblock_label = QLabel(_("selection.none_selected"))
self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
sponsorblock_layout.addWidget(self.selected_sponsorblock_label)
sponsorblock_layout.addStretch()
# Add the sponsorblock layout to the main video info layout
video_info_layout.addLayout(sponsorblock_layout)
# --- End SponsorBlock Section ---
# Initialize SponsorBlock categories as empty initially (will be set to defaults when user opens dialog)
self.selected_sponsorblock_categories = []
self._update_sponsorblock_display()
# Add stretch at the bottom
video_info_layout.addStretch()
# Add video info layout to main layout
media_info_layout.addLayout(video_info_layout, stretch=1)
return media_info_layout
def setup_playlist_info_section(self) -> QLabel:
self = cast("YTSageApp", self) # for autocompletion and type inference.
self.playlist_info_label = QLabel()
self.playlist_info_label.setVisible(False)
self.playlist_info_label.setStyleSheet(
"""
QLabel {
font-size: 12px;
color: #ffffff;
padding: 5px 8px;
margin: 0;
background-color: #1d1e22;
border: 1px solid #c90000;
border-radius: 4px;
min-height: 30px;
max-height: 30px;
}
"""
)
self.playlist_info_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
return self.playlist_info_label
def update_video_info(self, info) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
if hasattr(self, "is_playlist") and self.is_playlist:
# Playlist Mode: Show playlist title and video count
self.title_label.setText(self.playlist_info.get("title", _("playlist.unknown")))
num_videos = len(getattr(self, "playlist_entries", []))
self.duration_label.setText(_("playlist.total_videos", count=num_videos))
# Hide video-specific info
self.channel_label.setText("")
self.views_label.setText("")
self.date_label.setText("")
self.like_count_label.setText("")
self.channel_label.setVisible(False)
self.views_label.setVisible(False)
self.date_label.setVisible(False)
self.like_count_label.setVisible(False)
else:
# Single Video Mode: Show standard video info
# Ensure labels are visible first
self.channel_label.setVisible(True)
self.views_label.setVisible(True)
self.date_label.setVisible(True)
self.like_count_label.setVisible(True)
# Format view count with commas
views = info.get("view_count")
formatted_views = f"{views:,}" if views is not None else "N/A"
# Format like count with commas
likes = info.get("like_count")
formatted_likes = f"{likes:,}" if likes is not None else "N/A"
# Format upload date
upload_date = info.get("upload_date", "")
if upload_date:
date_obj = datetime.strptime(upload_date, "%Y%m%d")
formatted_date = date_obj.strftime("%B %d, %Y")
else:
formatted_date = _("video_info.unknown_date")
# Format duration
duration = info.get("duration", 0)
minutes = duration // 60
seconds = duration % 60
duration_str = f"{minutes}:{seconds:02d}"
# Update labels with localized text
self.title_label.setText(info.get("title", _("video_info.unknown_title")))
self.channel_label.setText(f"{_("video_info.channel")}: {info.get('uploader', _("video_info.unknown_channel"))}")
self.views_label.setText(f"{_("video_info.views")}: {formatted_views}")
self.like_count_label.setText(f"{_("video_info.likes")}: {formatted_likes}")
self.date_label.setText(f"{_("video_info.upload_date")}: {formatted_date}")
self.duration_label.setText(f"{_("video_info.duration")}: {duration_str}")
def open_subtitle_dialog(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
if not hasattr(self, "available_subtitles") or not hasattr(self, "available_automatic_subtitles"):
logger.warning("Subtitle info not loaded yet.")
return
if not hasattr(self, "selected_subtitles"):
self.selected_subtitles = []
dialog = SubtitleSelectionDialog(
self.available_subtitles, # type: ignore[reportAttributeAccessIssue]
self.available_automatic_subtitles, # type: ignore[reportAttributeAccessIssue]
self.selected_subtitles,
self, # Parent for the dialog
)
# removed extra logic for mapping to main_windows
merge_checkbox = getattr(self, "merge_subs_checkbox", None)
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
count = len(self.selected_subtitles)
self.selected_subs_label.setText(_("subtitle_selection.count_selected", count=count))
self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0)
# Enable/disable the merge checkbox in the parent window
if merge_checkbox:
# Only enable merge checkbox if we're not in Audio Only mode and analysis is complete
is_audio_only = hasattr(self, "audio_button") and self.audio_button.isChecked()
has_analysis = getattr(self, "analysis_completed", False)
# In audio-only mode, we still allow subtitle selection but not merging
should_enable = count > 0 and not is_audio_only and has_analysis
merge_checkbox.setEnabled(should_enable)
# Update tooltip
if not has_analysis:
merge_checkbox.setToolTip(_("main_ui.analyze_first_tooltip"))
elif is_audio_only:
merge_checkbox.setToolTip(_("main_ui.audio_mode_disabled"))
elif count == 0:
merge_checkbox.setToolTip(_("main_ui.select_subtitles_first"))
else:
merge_checkbox.setToolTip("")
else:
logger.warning("merge_subs_checkbox not found on parent window.")
# Re-apply stylesheet to update button border if property changed
self.subtitle_select_btn.style().unpolish(self.subtitle_select_btn)
self.subtitle_select_btn.style().polish(self.subtitle_select_btn)
# No else needed for cancel, state remains unchanged
def open_sponsorblock_dialog(self) -> None:
"""Open the SponsorBlock category selection dialog."""
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Initialize selected categories if not exists or empty (first time opening)
if not hasattr(self, "selected_sponsorblock_categories") or not self.selected_sponsorblock_categories:
# Use None to let the dialog set its own defaults
dialog_categories = None
else:
dialog_categories = self.selected_sponsorblock_categories
dialog = SponsorBlockCategoryDialog(dialog_categories, self)
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()
def _update_sponsorblock_display(self) -> None:
"""Update the SponsorBlock button and label to reflect current selection."""
self = cast("YTSageApp", self) # for autocompletion and type inference.
if not hasattr(self, "selected_sponsorblock_categories"):
self.selected_sponsorblock_categories = []
count = len(self.selected_sponsorblock_categories)
# Update label text
if count == 0:
self.selected_sponsorblock_label.setText(_("selection.none_selected"))
elif count == 1:
self.selected_sponsorblock_label.setText(_("selection.one_selected"))
else:
self.selected_sponsorblock_label.setText(_("selection.count_selected", count=count))
# Update button property for styling
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0)
# Force style refresh
self.sponsorblock_select_btn.style().unpolish(self.sponsorblock_select_btn)
self.sponsorblock_select_btn.style().polish(self.sponsorblock_select_btn)
def download_thumbnail(self, url) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
# Store both thumbnail URL and video URL
self.thumbnail_url = url
self.video_url = self.url_input.text() # Get actual video URL
# 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)
img_byte_arr = BytesIO()
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 processing thumbnail image: {e}")
def download_thumbnail_file(self, video_url, path) -> bool:
self = cast("YTSageApp", self) # for autocompletion and type inference.
if not self.save_thumbnail:
return False
try:
# Use cached thumbnail image from analysis if available
if self.thumbnail_image is None:
logger.info("No thumbnail image cached from analysis")
self.signals.update_status.emit(_("status.thumbnail_no_image"))
return False
# Get video title from cached video_info
video_title = "thumbnail"
if self.video_info and "title" in self.video_info:
video_title = self.video_info["title"]
elif self.playlist_info and "title" in self.playlist_info:
video_title = self.playlist_info["title"]
logger.debug(f"Saving cached thumbnail for: {video_title}")
# Save the thumbnail
thumb_dir = Path(path).joinpath("Thumbnails")
thumb_dir.mkdir(exist_ok=True)
filename = f"{self.sanitize_filename(video_title)}.jpg"
thumbnail_path = thumb_dir.joinpath(filename)
# Save the cached PIL Image directly
# Convert to RGB if necessary (in case of RGBA or other modes)
if self.thumbnail_image.mode in ("RGBA", "P"):
rgb_image = self.thumbnail_image.convert("RGB")
rgb_image.save(thumbnail_path, "JPEG", quality=95)
else:
self.thumbnail_image.save(thumbnail_path, "JPEG", quality=95)
logger.info(f"Thumbnail saved to: {thumbnail_path}")
self.signals.update_status.emit(_("status.thumbnail_saved", filename=filename))
return True
except Exception as e:
logger.exception(f"Thumbnail Save Error: {e}")
self.signals.update_status.emit(_("status.thumbnail_error", error=str(e)))
return False
def sanitize_filename(self, name) -> str:
"""Clean filename for filesystem safety"""
return re.sub(r'[\\/*?:"<>|]', "", name).strip()[:75]
+123
View File
@@ -0,0 +1,123 @@
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import (
QFrame,
QGraphicsOpacityEffect,
QLabel,
QStackedWidget,
QTabBar,
QVBoxLayout,
QWidget,
)
class FadingStackedWidget(QStackedWidget):
"""
A QStackedWidget that cross-fades between widgets.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.fade_duration = 300
self.fade_easing = QEasingCurve.Type.OutQuad
def setCurrentIndex(self, index):
curr_index = self.currentIndex()
if index == curr_index:
return
widget = self.widget(index)
curr_widget = self.widget(curr_index)
# If widget isn't visible or valid, just swap
if not self.isVisible() or not curr_widget:
super().setCurrentIndex(index)
return
# 1. Capture the current view (the "old" tab)
# Use grab() for simplicity and reliability in PySide6
pixmap = self.grab()
# 2. Create an overlay label to hold this "old" view
overlay = QLabel(self)
overlay.setPixmap(pixmap)
overlay.setGeometry(0, 0, self.width(), self.height())
overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) # Don't block clicks
overlay.show()
# 3. Switch the actual stack to the "new" view
super().setCurrentIndex(index)
# CRITICAL: Ensure overlay stays on top of the new widget
overlay.raise_()
# 4. Fade OUT the overlay, revealing the new view
effect = QGraphicsOpacityEffect(overlay)
overlay.setGraphicsEffect(effect)
anim = QPropertyAnimation(effect, b"opacity", overlay)
anim.setDuration(self.fade_duration)
anim.setStartValue(1.0)
anim.setEndValue(0.0)
anim.setEasingCurve(self.fade_easing)
# Cleanup when done
anim.finished.connect(lambda: self._cleanup(overlay))
# Keep reference to prevent GC
self._active_anim = anim
anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
def _cleanup(self, overlay):
overlay.hide()
overlay.deleteLater()
class SmoothTabWidget(QWidget):
"""
A unified Widget that behaves like a QTabWidget but uses smooth fading transitions.
Includes a QTabBar and a FadingStackedWidget.
"""
def __init__(self, parent=None):
super().__init__(parent)
# Main Layout
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Tab Bar
self.tab_bar = QTabBar(self)
self.tab_bar.setDrawBase(False) # We draw border on content instead
self.tab_bar.currentChanged.connect(self.set_current_index)
self.layout.addWidget(self.tab_bar)
# Content Area (Frame) - Mimics QTabWidget::pane
self.content_frame = QFrame(self)
self.content_frame.setObjectName("tabContent")
# Layout inside the content frame
self.content_layout = QVBoxLayout(self.content_frame)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_layout.setSpacing(0)
# The Stack
self.stack = FadingStackedWidget(self.content_frame)
self.content_layout.addWidget(self.stack)
self.layout.addWidget(self.content_frame)
def addTab(self, widget, label):
"""Add a tab with the given widget and label."""
self.stack.addWidget(widget)
self.tab_bar.addTab(label)
def set_current_index(self, index):
"""Slot to handle tab bar clicks."""
self.tab_bar.setCurrentIndex(index)
self.stack.setCurrentIndex(index)
def currentWidget(self):
return self.stack.currentWidget()
def currentIndex(self):
return self.stack.currentIndex()
+432
View File
@@ -0,0 +1,432 @@
class StyleSheet:
MAIN = """
QMainWindow {
background-color: #15181b;
}
QWidget {
background-color: #15181b;
color: #ffffff;
}
QLineEdit {
padding: 5px 15px;
border: 2px solid #2a2d2e;
border-radius: 6px;
background-color: #1b2021;
color: #ffffff;
font-size: 13px;
}
QLineEdit:focus {
border-color: #ff6b6b;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
padding: 10px 13px 6px 17px;
}
QPushButton:disabled {
background-color: #3d3d3d;
color: #888888;
}
QTableWidget {
border: 2px solid #1b2021;
border-radius: 4px;
background-color: #1b2021;
gridline-color: #1b2021;
}
QHeaderView::section {
background-color: #15181b;
padding: 5px;
border: 1px solid #1b2021;
color: #ffffff;
}
QProgressBar {
border: 2px solid #1b2021;
border-radius: 4px;
text-align: center;
color: white;
}
QProgressBar::chunk {
background-color: #c90000;
border-radius: 2px;
}
QLabel {
color: #ffffff;
}
/* Style for filter buttons */
QPushButton.filter-btn {
background-color: #1b2021;
padding: 5px 10px;
margin: 0 5px;
}
QPushButton.filter-btn:checked {
background-color: #c90000;
}
QPushButton.filter-btn:hover {
background-color: #444444;
}
QPushButton.filter-btn:checked:hover {
background-color: #a50000;
}
/* Modern Scrollbar Styling */
QScrollBar:vertical {
border: none;
background: #15181b;
width: 14px;
margin: 15px 0 15px 0;
border-radius: 7px;
}
QScrollBar::handle:vertical {
background: #404040;
min-height: 30px;
border-radius: 7px;
}
QScrollBar::handle:vertical:hover {
background: #505050;
}
QScrollBar::sub-line:vertical {
border: none;
background: #15181b;
height: 15px;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:vertical {
border: none;
background: #15181b;
height: 15px;
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical:hover,
QScrollBar::add-line:vertical:hover {
background: #404040;
}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
background: none;
width: 0;
height: 0;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: none;
}
"""
PASTE_BUTTON = """
QPushButton {
padding: 9px 20px;
background-color: #1b2021;
border: 2px solid #2a2d2e;
border-radius: 5px;
color: #ffffff;
font-weight: 600;
font-size: 13px;
}
QPushButton:hover {
background-color: #252829;
border-color: #3a3d3e;
}
QPushButton:pressed {
background-color: #1a1d1e;
padding: 11px 18px 7px 22px;
}
"""
ANALYZE_BUTTON = """
QPushButton {
padding: 9px 20px;
background-color: #c90000;
border: none;
border-radius: 5px;
color: white;
font-weight: 600;
font-size: 13px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
padding: 11px 18px 7px 22px;
}
QPushButton:disabled {
background-color: #3d3d3d;
color: #888888;
}
"""
PLAYLIST_BUTTON = """
QPushButton {
padding: 6px 12px;
background-color: #1d1e22;
border: 1px solid #c90000;
border-radius: 4px;
color: white;
font-weight: normal;
text-align: left;
padding-left: 10px;
}
QPushButton:hover {
background-color: #2a2d36;
border-color: #a50000;
}
QPushButton:pressed {
background-color: #1d1e22;
padding: 8px 10px 4px 12px;
}
"""
FORMAT_TOGGLE_BUTTON = """
QPushButton {
padding: 8px 15px;
background-color: #1d1e22;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:checked {
background-color: #c90000;
}
QPushButton:hover {
background-color: #2a2d36;
}
QPushButton:checked:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #151619;
padding: 10px 13px 6px 17px;
}
QPushButton:checked:pressed {
background-color: #800000;
padding: 10px 13px 6px 17px;
}
"""
CHECKBOX = """
QCheckBox {
color: #ffffff;
padding: 5px;
margin-left: 20px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
QCheckBox:disabled { color: #888888; }
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
"""
PROGRESS_BAR = """
QProgressBar {
border: 2px solid #3d3d3d;
border-radius: 4px;
text-align: center;
color: white;
background-color: #363636;
height: 25px;
}
QProgressBar::chunk {
background-color: #ff0000;
border-radius: 2px;
}
"""
STATUS_LABEL = """
QLabel {
color: #cccccc;
font-size: 12px;
padding: 5px;
}
"""
OPEN_FOLDER_BUTTON = """
QPushButton {
background-color: #2a2d2e;
color: #cccccc;
border: 1px solid #404040;
border-radius: 5px;
font-size: 16px;
padding: 2px;
}
QPushButton:hover {
background-color: #3a3d3e;
border: 1px solid #505050;
}
QPushButton:pressed {
background-color: #1a1d1e;
padding: 4px 0px 0px 4px;
}
"""
UPDATE_DIALOG_MESSAGE = """
QLabel {
background-color: #1d1e22;
border: 1px solid #3d3d3d;
border-radius: 6px;
padding: 15px;
margin: 5px 0;
}
"""
UPDATE_DIALOG_CHANGELOG = """
QTextEdit {
background-color: #1d1e22;
border: 2px solid #3d3d3d;
border-radius: 6px;
color: #ffffff;
padding: 10px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 12px;
line-height: 1.4;
}
QScrollBar:vertical {
border: none;
background: #1d1e22;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background: #404040;
min-height: 20px;
border-radius: 6px;
}
QScrollBar::handle:vertical:hover {
background: #505050;
}
"""
UPDATE_DIALOG_DOWNLOAD_BTN = """
QPushButton {
padding: 10px 20px;
background-color: #c90000;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-width: 140px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:pressed {
background-color: #800000;
}
"""
UPDATE_DIALOG_REMIND_BTN = """
QPushButton {
padding: 10px 20px;
background-color: #3d3d3d;
border: 1px solid #555555;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 13px;
min-width: 140px;
}
QPushButton:hover {
background-color: #4d4d4d;
border-color: #666666;
}
QPushButton:pressed {
background-color: #2d2d2d;
}
"""
UPDATE_DIALOG_MAIN = """
QDialog {
background-color: #15181b;
border: 1px solid #3d3d3d;
border-radius: 8px;
}
QLabel {
color: #ffffff;
font-size: 12px;
}
"""
TIME_RANGE_BTN_ACTIVE = """
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
border: 2px solid white;
}
QPushButton:hover {
background-color: #a50000;
}
"""
FILE_EXISTS_DIALOG = """
QMessageBox {
background-color: #2b2b2b;
}
QLabel {
color: #ffffff;
}
QPushButton {
padding: 8px 15px;
background-color: #ff0000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #cc0000;
}
"""
SETUP_SUCCESS_DIALOG = """
QMessageBox {
background-color: #15181b;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
"""
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "العربية (Arabic)",
"select_language": "اختر اللغة:",
"current_language": "اللغة الحالية: {language}",
"restart_required": "سيتم تطبيق تغيير اللغة بعد إعادة تشغيل التطبيق.",
"english": "الإنجليزية",
"spanish": "الإسبانية",
"portuguese": "البرتغالية",
"russian": "الروسية",
"chinese": "الصينية",
"german": "الألمانية",
"french": "الفرنسية",
"hindi": "الهندية",
"indonesian": "الإندونيسية",
"turkish": "التركية",
"polish": "البولندية",
"italian": "الإيطالية",
"arabic": "العربية",
"japanese": "اليابانية",
"help_text": "اختر اللغة المفضلة للواجهة.",
"restart_notice": "سيتم تطبيق تغيير اللغة بعد إعادة تشغيل التطبيق."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "جاهز"
},
"formats": {
"show_formats": "إظهار التنسيقات:",
"video_format": "تنسيق الفيديو:",
"audio_format": "تنسيق الصوت:",
"no_formats": "لا توجد تنسيقات متاحة",
"loading": "جاري تحميل التنسيقات...",
"select": "اختر",
"quality": "الجودة",
"extension": "الامتداد",
"resolution": "الدقة",
"file_size": "حجم الملف",
"codec": "الترميز",
"audio": "الصوت",
"fps": "إطار/ث",
"hdr": "HDR",
"will_merge_audio": "سيتم دمج الصوت",
"has_audio": "✓ يحتوي على صوت",
"audio_only": "صوت فقط",
"best_4k": "الأفضل (4K)",
"best_2k": "الأفضل (2K)",
"high_1080p": "عالية (1080p)",
"high_720p": "عالية (720p)",
"medium_480p": "متوسطة (480p)",
"low_quality": "جودة منخفضة",
"best_audio": "أفضل صوت",
"high_audio": "صوت عالي",
"medium_audio": "صوت متوسط",
"low_audio": "صوت منخفض",
"audio_only_resolution": "صوت فقط"
},
"buttons": {
"reset": "إعادة تعيين",
"download": "تنزيل",
"pause": "إيقاف مؤقت",
"resume": "استئناف",
"cancel": "إلغاء",
"browse": "تصفح",
"clear": "مسح",
"ok": "موافق",
"apply": "تطبيق",
"close": "إغلاق",
"run_command": "تشغيل الأمر",
"custom_command_help": "مساعدة",
"about": "حول",
"analyze": "تحليل",
"paste_url": "لصق الرابط",
"select_videos": "اختر الفيديوهات...",
"video": "فيديو",
"audio_only": "صوت فقط",
"custom_options": "خيارات مخصصة",
"trim_video": "قص الفيديو",
"download_settings": "إعدادات التنزيل",
"update": "تحديث yt-dlp",
"select_defaults": "اختر الافتراضي",
"select_all": "تحديد الكل",
"deselect_all": "إلغاء تحديد الكل",
"open_folder": "فتح موقع المجلد",
"history": "السجل",
"save_playlist": "حفظ قائمة التشغيل باسم"
},
"dialogs": {
"custom_options": "خيارات مخصصة",
"settings": "الإعدادات",
"select_folder": "اختر مجلد التنزيل",
"sponsorblock_categories": "فئات SponsorBlock",
"sponsorblock_description": "اختر أنواع مقاطع الفيديو المراد إزالتها تلقائياً أثناء التنزيل.\nيستخدم SponsorBlock بيانات مقدمة من المجتمع لتحديد هذه المقاطع.",
"select_subtitles": "اختر الترجمات",
"filter_languages_placeholder": "تصفية اللغات (مثال: ar، en)...",
"no_subtitles_available": "لا توجد ترجمات متاحة",
"matching": "مطابقة",
"ytdlp_log_title": "سجل yt-dlp",
"filter_playlist_placeholder": "تصفية الفيديوهات..."
},
"tabs": {
"cookies": "تسجيل الدخول بالكوكيز",
"custom_command": "أمر مخصص",
"proxy": "بروكسي",
"language": "اللغة",
"updater": "التحديث"
},
"cookies": {
"help_text": "اختر طريقة لتوفير ملفات تعريف الارتباط للمصادقة.\nيسمح هذا بتنزيل مقاطع الفيديو الخاصة وملفات الصوت عالية الجودة.",
"cookie_source": "مصدر الكوكيز",
"use_cookie_file": "استخدام ملف كوكيز",
"extract_from_browser": "استخراج من المتصفح",
"recommended": "موصى به",
"cookie_file": "ملف الكوكيز",
"cookie_file_placeholder": "مسار ملف cookies.txt...",
"browser_selection": "اختيار المتصفح",
"browser_help": "اختر المتصفح لاستخراج ملفات تعريف الارتباط منه:",
"browser_label": "المتصفح:",
"profile_label": "الملف الشخصي:",
"profile_placeholder": "افتراضي",
"browser_extract_message": "سيتم استخراج كوكيز المتصفح بعد التطبيق",
"file_selected_message": "تم تحديد ملف الكوكيز - انقر على موافق للتطبيق",
"select_file_title": "اختر ملف الكوكيز",
"file_filter": "ملفات الكوكيز (*.txt *.lwp)",
"file_selected_title": "تم تطبيق ملف الكوكيز",
"file_applied_message": "تم تطبيق ملف الكوكيز: {path}",
"browser_selected_title": "تم تطبيق كوكيز المتصفح",
"browser_applied_message": "سيتم استخراج كوكيز المتصفح من: {browser}",
"cleared_title": "تم مسح الكوكيز",
"cleared_message": "تم مسح إعدادات الكوكيز",
"active_browser": "✓ نشط: كوكيز المتصفح ({browser})",
"active_file": "✓ نشط: ملف كوكيز ({file})",
"none_active": "○ لا توجد كوكيز نشطة",
"remember_settings": "تذكر إعدادات ملفات تعريف الارتباط عند بدء التشغيل التالي"
},
"custom_command": {
"help_text": "أدخل أمر yt-dlp مخصص أدناه. سيتم إضافة الرابط الحالي تلقائياً.<br><br>للحصول على قائمة كاملة بالخيارات وأمثلة الاستخدام <a href=\"{docs_url}\">انقر هنا لمشاهدة الوثائق الرسمية لـ yt-dlp</a>.<br><br>ملاحظة: يتم التعامل مع مسار التنزيل ونموذج اسم الملف تلقائياً.",
"input_label": "معاملات yt-dlp:",
"input_placeholder": "أدخل معاملات yt-dlp هنا...\n\nمثال: --extract-audio --audio-format mp3",
"output_label": "مخرجات الأمر:",
"output_placeholder": "ستظهر مخرجات الأمر هنا...",
"command_placeholder": "أدخل أمر yt-dlp مخصص...",
"command_help": "العناصر المتاحة:\n{url} - رابط الفيديو\n{output} - مجلد الإخراج\n\nمثال: --write-info-json --write-thumbnail",
"full_command": "🔧 الأمر الكامل: {command}",
"command_success": "✅ تم تنفيذ الأمر المخصص بنجاح!",
"command_failed": "❌ فشل الأمر برمز الخروج {code}",
"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": "قم بتكوين إعدادات البروكسي للتنزيلات. اتركه فارغاً للاتصال المباشر.",
"main_proxy": "البروكسي الرئيسي",
"main_proxy_help": "خادم البروكسي الرئيسي لجميع التنزيلات. يدعم بروتوكولات HTTP/HTTPS و SOCKS5.",
"proxy_url_label": "رابط البروكسي:",
"proxy_url_placeholder": "http://proxy:port أو socks5://proxy:port",
"proxy_examples": "أمثلة:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "بروكسي تجاوز الموقع الجغرافي",
"geo_proxy_help": "بروكسي إضافي خصيصاً لتجاوز القيود الجغرافية.",
"geo_proxy_url_label": "رابط بروكسي تجاوز الموقع:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "مسح البروكسي الرئيسي",
"clear_geo_proxy": "مسح بروكسي الموقع",
"proxy_url": "رابط البروكسي",
"proxy_placeholder": "http://proxy:port أو socks5://proxy:port",
"geo_bypass": "بروكسي تجاوز الموقع الجغرافي (للقيود الجغرافية)",
"geo_bypass_placeholder": "http://proxy:port لتجاوز الحظر الجغرافي",
"invalid_main_url": "تنسيق رابط البروكسي الرئيسي غير صالح",
"invalid_geo_url": "تنسيق رابط بروكسي الموقع غير صالح",
"main_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": "جاري التحضير للتنزيل...",
"starting": "🚀 بدء التنزيل...",
"fetching_info": "🔍 جاري الحصول على معلومات الفيديو...",
"preparing_streams": "🎯 جاري تحضير تدفقات الفيديو...",
"downloading_audio": "⏬ جاري تنزيل الصوت...",
"downloading_video": "⏬ جاري تنزيل الفيديو...",
"downloading_subtitle": "⏬ جاري تنزيل الترجمات...",
"downloading": "⏬ جاري التنزيل...",
"completed": "✅ اكتمل التنزيل!",
"video_completed": "✅ اكتمل تنزيل الفيديو!",
"audio_completed": "✅ اكتمل تنزيل الصوت!",
"subtitle_completed": "✅ اكتمل تنزيل الترجمات!",
"completed_cleaning": "✅ اكتمل التنزيل! جاري التنظيف...",
"cancelled": "تم إلغاء التنزيل",
"processing_playlist": "📋 جاري معالجة بيانات قائمة التشغيل...",
"paused": "تم إيقاف التنزيل مؤقتاً",
"resumed": "تم استئناف التنزيل",
"merging_formats": "✨ المعالجة اللاحقة: دمج التنسيقات...",
"removing_sponsor_segments": "✨ المعالجة اللاحقة: إزالة مقاطع الرعاية...",
"speed": "السرعة",
"eta": "الوقت المتبقي",
"please_enter_url": "الرجاء إدخال رابط أولاً.",
"please_set_path": "الرجاء تحديد مسار التنزيل أولاً.",
"please_enter_url_and_path": "الرجاء إدخال رابط وتحديد مسار التنزيل.",
"please_select_format": "الرجاء اختيار تنسيق أولاً.",
"downloading_fallback": "⚡ جاري التنزيل..."
},
"update": {
"title": "تحديث yt-dlp",
"checking": "جاري التحقق من التحديثات...",
"update_available": "تحديث متاح!\nالإصدار الحالي: {current}\nأحدث إصدار: {latest}",
"up_to_date": "yt-dlp محدث (الإصدار {version})",
"could_not_determine": "تعذر تحديد الإصدار.",
"error_comparing": "خطأ في مقارنة الإصدارات: {error}",
"update_available_failed": "تحديث متاح! (فشلت المقارنة)\nالحالي: {current}\nالأحدث: {latest}",
"updating": "جاري التحديث...",
"initializing": "🚀 جاري تهيئة عملية التحديث...",
"checking_current": "🔍 جاري التحقق من التثبيت الحالي...",
"found_at": "📍 تم العثور على yt-dlp في: {path}",
"error_getting_path": "❌ خطأ في الحصول على مسار yt-dlp: {error}",
"updating_binary": "📦 جاري تحديث ملف yt-dlp الثنائي المدار بواسطة التطبيق...",
"update_failed": "❌ فشل تحديث yt-dlp. حاول مرة أخرى أو تحقق من اتصال الإنترنت.",
"binary_updated": "✅ تم تحديث الملف الثنائي بنجاح!",
"update_failed_stderr": "❌ فشل تحديث yt-dlp: {error}",
"update_timeout": "❌ انتهت مهلة تحديث yt-dlp.",
"unexpected_error": "❌ خطأ غير متوقع أثناء التحديث: {error}",
"already_up_to_date": "✅ yt-dlp محدث بالفعل!",
"update_success": "✅ تم تحديث yt-dlp بنجاح!",
"already_latest": "yt-dlp محدث (الإصدار {version})",
"network_error": "❌ خطأ في الشبكة أثناء التحديث: {error}",
"general_error": "❌ فشل التحديث: {error}",
"update_in_progress_title": "التحديث قيد التقدم",
"update_in_progress_message": "يتم تحديث yt-dlp حاليًا. يرجى الانتظار لحظة."
},
"about": {
"title": "حول YTSage",
"version": "الإصدار {version}",
"description": "برنامج تنزيل يوتيوب حديث مع واجهة PySide6 نظيفة.",
"author": "المطور: {author}",
"github": "GitHub: {repo}",
"system_info": "معلومات النظام",
"loading": "🔄 جاري تحميل معلومات النظام...",
"refresh": "🔄",
"open_logs": "📂 السجلات",
"logs_tooltip": "فتح مجلد سجلات التطبيق",
"refreshing": "🔄 جاري التحديث...",
"refresh_failed": "فشل التحديث",
"refresh_failed_message": "تعذر تحديث معلومات الإصدار.",
"detected": "✓ تم الكشف",
"missing": "✗ مفقود",
"not_available": "غير متاح"
},
"time_range": {
"title": "قص الفيديو",
"time_range_group": "النطاق الزمني",
"start_time": "وقت البدء (HH:MM:SS)",
"end_time": "وقت الانتهاء (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "وقت البدء (HH:MM:SS)",
"end_time_placeholder": "وقت الانتهاء (HH:MM:SS)",
"force_keyframes": "فرض الإطارات الرئيسية عند نقاط القص",
"help_text": "حدد أوقات البدء والانتهاء لقص الفيديو.\nاتركه فارغاً لتنزيل الفيديو كاملاً.",
"invalid_format": "تنسيق الوقت غير صالح. استخدم تنسيق HH:MM:SS.",
"start_after_end": "لا يمكن أن يكون وقت البدء بعد وقت الانتهاء."
},
"settings": {
"title": "إعدادات التنزيل",
"download_path": "مسار التنزيل",
"browse": "تصفح...",
"speed_limit": "حد السرعة",
"speed_limit_placeholder": "بدون",
"generic_mode": "الوضع العام",
"enable_generic_mode": "تفعيل الوضع العام (دعم المواقع غير التابعة ليوتيوب)",
"generic_mode_help": "يسمح بالتنزيل من Dailymotion وCBC Gem ومواقع أخرى يدعمها yt-dlp.",
"auto_update_ytdlp": "التحديثات التلقائية لـ yt-dlp",
"enable_auto_updates": "تفعيل التحديثات التلقائية لـ yt-dlp",
"update_frequency": "تكرار التحديثات:",
"check_startup": "تحقق عند كل بداية تشغيل (ساعة واحدة على الأقل بين الفحوصات)",
"check_daily": "تحقق يومياً",
"check_weekly": "تحقق أسبوعياً",
"check_updates_now": "تحديث yt-dlp",
"update_check_title": "التحقق من التحديثات",
"could_not_determine_version": "تعذر تحديد الإصدار الحالي لـ yt-dlp.",
"update_available_dialog": "تحديث متاح!\n\nالحالي: {current}\nالأحدث: {latest}\n\nانقر فوق موافق للمتابعة بالتحديث.",
"up_to_date_dialog": "yt-dlp محدث!\n\nالإصدار الحالي: {version}",
"error_checking_updates": "خطأ في التحقق من التحديثات: {error}",
"settings_saved_title": "تم حفظ الإعدادات",
"settings_saved_message": "تم حفظ إعدادات التحديث التلقائي بنجاح!",
"error_title": "خطأ",
"failed_save_settings": "فشل حفظ إعدادات التحديث التلقائي.",
"error_saving_settings": "خطأ في حفظ إعدادات التحديث التلقائي: {error}",
"ytdlp_channel": "قناة إصدار yt-dlp",
"ytdlp_channel_stable": "مستقر (إصدارات مختبرة)",
"ytdlp_channel_nightly": "ليلي (تحديثات يومية، موصى به من yt-dlp)",
"ytdlp_channel_description": "اختر بين الإصدارات المستقرة والليلية. يتم تحديث الإصدارات الليلية يومياً بأحدث الإصلاحات والميزات. يمكنك تبديل القنوات في أي وقت.",
"ytdlp_switching_channel": "التبديل إلى قناة {channel}...",
"ytdlp_channel_switched": "✅ تم التبديل بنجاح إلى قناة {channel}!",
"ytdlp_channel_switch_failed": "❌ فشل تبديل القناة: {error}",
"ytdlp_current_channel": "القناة الحالية: {channel}",
"app_updates_title": "تحديثات YTSage",
"check_app_updates": "التحقق من تحديثات YTSage عند بدء التشغيل",
"check_beta_updates": "تلقي تحديثات تجريبية (Beta)",
"auto_update_title": "إعدادات التحديثات التلقائية",
"auto_update_header": "🔄 إعدادات التحديثات التلقائية",
"auto_update_description": "قم بتكوين التحديثات التلقائية لـ yt-dlp لضمان الحصول على أحدث الميزات وإصلاحات الأخطاء.",
"current_status": "الحالة الحالية",
"current_version_label": "إصدار yt-dlp الحالي: جاري التحقق...",
"last_check_label": "آخر فحص للتحديثات: أبداً",
"next_check_label": "الفحص التالي: بناءً على الإعدادات",
"manual_check_button": "🔍 تحقق من التحديثات الآن",
"update_frequency_group": "تكرار التحديثات",
"save_settings": "حفظ الإعدادات",
"settings_saved_successfully": "✅ تم حفظ الإعدادات بنجاح!",
"error_saving": "❌ خطأ في حفظ الإعدادات: {error}",
"output_format_settings": "إعدادات تنسيق الإخراج",
"force_output_format": "فرض تنسيق الإخراج عند الدمج",
"preferred_format": "التنسيق المفضل:",
"force_format_help": "عند التمكين، سيتم تحويل الفيديوهات المدموجة إلى تنسيقك المفضل. قم بالتعطيل للسماح لـ yt-dlp بالقرار تلقائياً.",
"format_mp4": "MP4 (الأكثر توافقاً)",
"format_webm": "WebM (حديث، مفتوح)",
"format_mkv": "MKV (غني بالميزات)",
"audio_format_settings": "إعدادات تنسيق الصوت",
"force_audio_format": "فرض تنسيق الصوت لتنزيلات الصوت فقط",
"audio_normalization": "تسوية الصوت (EBU R128)",
"audio_normalization_help": "عند التمكين، سيتم تسوية المسارات الصوتية. ملاحظة: هذا يتطلب إعادة الترميز، لذلك يجب فرض تنسيق صوتي محدد (مثل MP3 أو M4A).",
"preferred_audio_format": "تنسيق الصوت المفضل:",
"force_audio_format_help": "عند التمكين، سيتم تحويل تنزيلات الصوت فقط إلى التنسيق المفضل لديك. هذا ينطبق فقط عند تنزيل تنسيقات الصوت.",
"audio_format_best": "الأفضل (بدون تحويل)",
"audio_format_aac": "AAC (جيد للتحرير)",
"audio_format_mp3": "MP3 (عالمي)",
"audio_format_flac": "FLAC (بدون فقدان)",
"audio_format_wav": "WAV (غير مضغوط)",
"audio_format_opus": "Opus (فعال)",
"audio_format_m4a": "M4A (أبل)",
"audio_format_vorbis": "Vorbis (مفتوح)",
"filename_format": "تنسيق اسم الملف الناتج",
"filename_format_help": "المتغيرات المتاحة: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. يتم دعم صيغة قالب إخراج yt-dlp القياسية.",
"tab_general": "عام",
"tab_format": "تنسيق",
"tab_file": "ملف",
"concurrent_fragments": "اتصالات متزامنة",
"concurrent_fragments_help": "عدد الاتصالات لكل عملية تحميل. القيم الأعلى تتجاوز الاختناق ولكنها قد تسبب حظراً مؤقتاً إذا كانت عالية جداً. الافتراضي: 1.",
"defaults_settings": "إعدادات الاختيار الافتراضية",
"default_video_quality": "دقة الفيديو الافتراضية (الطول):",
"default_subtitle_language": "لغات الترجمة الافتراضية:",
"defaults_help": "اضبط ارتفاع الفيديو المفضل ولغات الترجمة (مفصولة بفاصلة). سيتم اختيارها تلقائيًا إذا كانت متوفرة."
},
"main_ui": {
"url_placeholder": "أدخل رابط فيديو يوتيوب أو قائمة تشغيل",
"url_placeholder_generic": "أدخل رابط فيديو أو قائمة تشغيل من أي موقع مدعوم",
"merge_subtitles": "دمج الترجمات",
"save_thumbnail": "حفظ الصورة المصغرة",
"save_description": "حفظ الوصف",
"embed_chapters": "تضمين الفصول",
"subtitles_selected": "{count} محدد",
"all_selected": "تم تحديد الكل",
"select_videos_all": "اختر الفيديوهات... (تم تحديد الكل)",
"please_enter_url": "الرجاء إدخال رابط أولاً",
"error_title": "خطأ",
"cookie_file_selected_title": "تم تحديد ملف الكوكيز",
"cookie_file_selected_message": "ملف الكوكيز المحدد: {path}",
"browser_cookies_selected_title": "تم تحديد كوكيز المتصفح",
"browser_cookies_selected_message": "سيتم استخراج كوكيز المتصفح من: {browser}",
"error_no_format_info": "خطأ: لا توجد معلومات تنسيق متاحة.",
"error_extract_info": "خطأ: تعذر استخراج معلومات الفيديو الأساسية. تحقق من الرابط.",
"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": "جاري استخراج المعلومات...",
"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": "يرجى تحديد الترجمة أولاً",
"settings_tooltip": "المسار الحالي: {path}\nحد السرعة: {speed_limit}",
"speed_limit_none": "بدون",
"open_folder_error": "تعذر فتح المجلد: {error}",
"time_range_set": "تم تعيين المقطع: {section}"
},
"sponsorblock": {
"sponsor": "الراعي",
"sponsor_desc": "الإعلانات المدفوعة والتوصيات المدفوعة والإعلانات المباشرة",
"selfpromo": "غير مدفوع/ترويج ذاتي",
"selfpromo_desc": "الترويج غير المدفوع لمحتوى المنشئ الخاص",
"interaction": "تذكير التفاعل",
"interaction_desc": "يُطلب من المشاهدين الإعجاب أو الاشتراك أو المتابعة على وسائل التواصل الاجتماعي",
"intro": "المقدمة",
"intro_desc": "مقدمة الفيديو التي يمكن تخطيها",
"outro": "الخاتمة/البطاقات النهائية",
"outro_desc": "الاعتمادات النهائية أو عندما ينتهي الفيديو",
"preview": "المعاينة/الملخص",
"preview_desc": "ملخص موجز لمقاطع الفيديو السابقة أو معاينة المحتوى القادم",
"music_offtopic": "قسم غير موسيقي",
"music_offtopic_desc": "لمقاطع الفيديو الموسيقية فقط. يشير إلى أقسام غير موسيقية",
"filler": "حشو جانبي",
"filler_desc": "مشاهد جانبية تُضاف فقط كحشو أو للفكاهة"
},
"video_info": {
"channel": "القناة",
"views": "👁 المشاهدات",
"likes": "👍 الإعجابات",
"upload_date": "📅 تاريخ الرفع",
"duration": "⏱ المدة",
"unknown_channel": "قناة غير معروفة",
"unknown_date": "تاريخ غير معروف",
"unknown_title": "عنوان غير معروف"
},
"command": {
"running": "جاري التشغيل...",
"run_command": "تشغيل الأمر"
},
"selection": {
"none_selected": "0 محدد",
"one_selected": "فئة واحدة محددة",
"count_selected": "{count} محدد"
},
"status": {
"ready": "جاهز",
"file_exists": "⚠️ الملف موجود بالفعل",
"video_file_exists": "⚠️ ملف الفيديو موجود بالفعل",
"audio_file_exists": "⚠️ ملف الصوت موجود بالفعل",
"subtitle_file_exists": "⚠️ ملف الترجمات موجود بالفعل",
"cancelling": "جاري إلغاء التنزيل...",
"thumbnail_saved": "✅ تم حفظ الصورة المصغرة: {filename}",
"thumbnail_error": "❌ خطأ في الصورة المصغرة: {error}",
"thumbnail_no_image": "لا توجد صورة مصغرة متاحة للحفظ"
},
"errors": {
"playlist_no_videos": "خطأ: قائمة التشغيل لا تحتوي على مقاطع فيديو صالحة.",
"playlist_no_url": "خطأ: تعذر الحصول على رابط أول فيديو في قائمة التشغيل.",
"ytdlp_not_found": "خطأ: لم يتم العثور على ملف yt-dlp التنفيذي. الرجاء تثبيت yt-dlp أولاً.",
"ytdlp_not_found_path": "خطأ: لم يتم العثور على ملف yt-dlp التنفيذي. قد يكون ذلك بسبب تثبيت غير صحيح أو مشكلة في PATH.",
"no_data_returned": "خطأ: لم يتم إرجاع بيانات من yt-dlp",
"no_format_info": "خطأ: لا توجد معلومات تنسيق متاحة.",
"analysis_timeout": "خطأ: انتهت مهلة التحليل. الرجاء المحاولة مرة أخرى.",
"invalid_speed_limit": "❌ خطأ: قيمة حد السرعة غير صالحة في الإعدادات.",
"ytdlp_failed": "خطأ: فشل yt-dlp: {error}",
"parse_failed": "خطأ: فشل تحليل مخرجات yt-dlp: {error}",
"analysis_failed": "خطأ: فشل التحليل: {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}",
"private_video": "قد يكون هذا الفيديو خاصًا. يرجى استخدام ملفات تعريف الارتباط من الخيارات المخصصة."
},
"update_dialog": {
"title": "تحديث متاح",
"new_version_available": "يتوفر إصدار جديد من YTSage!",
"current_version_label": "الإصدار الحالي:",
"latest_version_label": "أحدث إصدار:",
"changelog": "سجل التغييرات",
"download_update": "تنزيل التحديث",
"remind_later": "ذكرني لاحقاً"
},
"playlist": {
"unknown": "قائمة تشغيل غير معروفة",
"total_videos": "إجمالي الفيديوهات: {count}",
"display_format": "قائمة التشغيل: {title} | {count} فيديوهات",
"select_videos_title": "اختر مقاطع الفيديو من قائمة التشغيل",
"save_as": "حفظ قائمة التشغيل باسم",
"save_success_title": "تم بنجاح",
"saved_successfully": "تم حفظ قائمة التشغيل بنجاح.",
"save_error_title": "خطأ في الحفظ",
"no_videos_to_save": "لم يتم العثور على أي مقاطع فيديو لحفظها!",
"save_error_msg": "فشل في حفظ قائمة التشغيل."
},
"subtitle_selection": {
"count_selected": "{count} محدد"
},
"file_exists_dialog": {
"title": "الملف موجود بالفعل",
"message": "الملف موجود بالفعل:\n{filename}",
"info": "تم تنزيل هذا الفيديو بالفعل."
},
"auto_update": {
"last_check_never": "آخر فحص للتحديث: أبداً",
"last_check": "آخر فحص للتحديث: {time}",
"next_check_disabled": "الفحص التالي: معطل",
"next_check_startup": "الفحص التالي: عند بدء التشغيل",
"next_check_overdue": "الفحص التالي: الآن (متأخر)",
"next_check": "الفحص التالي: {time}",
"next_check_error": "الفحص التالي: خطأ في الحساب",
"checking": "🔄 جاري الفحص...",
"check_now": "🔍 التحقق من التحديثات الآن",
"current_version": "إصدار yt-dlp الحالي: {version}"
},
"url_validation": {
"empty_url": "لا يمكن أن يكون الرابط فارغًا",
"invalid_format": "تنسيق الرابط غير صالح",
"invalid_scheme": "يجب أن يبدأ الرابط بـ http:// أو https://",
"missing_domain": "رابط غير صالح: اسم النطاق مفقود",
"unsupported_platform": "YTSage يدعم فقط روابط YouTube و YouTube Music.\nالنطاق '{domain}' غير مدعوم.",
"invalid_youtu_be": "رابط youtu.be غير صالح: معرف الفيديو مفقود"
},
"ytdlp_errors": {
"private_video": "هذا فيديو خاص. يمكنك تنزيله عن طريق تسجيل الدخول إلى حسابك باستخدام ملفات تعريف الارتباط.\nانتقل إلى 'خيارات مخصصة' ← 'تسجيل الدخول باستخدام ملفات تعريف الارتباط' ← 'استخراج ملفات تعريف الارتباط من المتصفح' للمصادقة.",
"age_restricted": "هذا الفيديو مقيد بالعمر. تحتاج إلى تسجيل الدخول للوصول إليه.\nاستخدم 'خيارات مخصصة' ← 'تسجيل الدخول باستخدام ملفات تعريف الارتباط' للمصادقة بحسابك.",
"geo_blocked": "هذا الفيديو غير متاح في منطقتك (محظور جغرافيًا).\nقد تحتاج إلى استخدام VPN أو قد يكون الفيديو مقيدًا في بلدك.",
"video_unavailable": "تمت إزالة هذا الفيديو أو لم يعد متاحًا.\nربما تم حذف الفيديو بواسطة المُحمِّل أو إزالته بسبب انتهاك السياسات.",
"live_stream": "هذا بث مباشر لا يمكن تنزيله أثناء نشاطه.\nانتظر حتى ينتهي البث، ثم حاول تنزيل النسخة المؤرشفة.",
"playlist_error": "تعذر الوصول إلى قائمة التشغيل هذه. قد تكون خاصة أو محذوفة أو فارغة.\nتحقق من وجود قائمة التشغيل وأنها متاحة للعامة.",
"network_error": "خطأ في الاتصال بالشبكة. يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.\nإذا استمرت المشكلة، فقد يكون خادم الفيديو غير متاح مؤقتًا.",
"invalid_url": "رابط غير صالح أو غير مدعوم. يرجى التحقق من الرابط والمحاولة مرة أخرى.\nتأكد من استخدام رابط صالح لـ YouTube أو Vimeo أو منصة مدعومة أخرى.",
"premium_content": "يتطلب هذا المحتوى YouTube Premium أو عضوية القناة.\nتحتاج إلى تسجيل الدخول بحساب لديه حق الوصول إلى هذا المحتوى.",
"copyright_blocked": "هذا الفيديو محظور بسبب مطالبات حقوق النشر.\nصاحب المحتوى قيد الوصول إلى هذا الفيديو.",
"extraction_failed": "فشل استخراج معلومات الفيديو. قد تكون هذه مشكلة مؤقتة.\nيرجى المحاولة مرة أخرى بعد بضع دقائق، أو تحقق من صحة رابط الفيديو.",
"generic_error": "تعذر استخراج معلومات الفيديو. يرجى التحقق من الرابط الخاص بك.\nالتفاصيل التقنية: {error}"
},
"history": {
"title": "سجل التنزيلات",
"clear_all": "مسح الكل",
"clear_confirm_title": "مسح السجل؟",
"clear_confirm_message": "هل أنت متأكد؟ لا يمكن التراجع عن هذا.",
"no_history": "لا يوجد سجل بعد",
"no_history_description": "ستظهر تنزيلاتك هنا",
"loading": "جارٍ تحميل السجل...",
"search_placeholder": "بحث...",
"open_location": "فتح الموقع",
"redownload": "إعادة التنزيل",
"remove": "إزالة",
"file_not_found": "الملف غير موجود",
"file_not_found_message": "تم نقل الملف أو حذفه:\n{path}",
"downloaded_on": "تم التنزيل: {date}",
"file_size": "الحجم: {size}",
"audio_download": "صوت",
"video_download": "فيديو",
"remove_confirm_title": "إزالة؟",
"remove_confirm_message": "إزالة من السجل؟\n\n{title}",
"item_removed": "تمت الإزالة",
"history_cleared": "تم مسح السجل",
"entries_count": "{count} تنزيلات",
"one_entry": "تنزيل واحد",
"redownload_confirm_title": "إعادة التنزيل؟",
"redownload_confirm_message": "إعادة التنزيل؟\n\n{title}",
"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",
"current_version": "الإصدار الحالي:",
"latest_version": "أحدث إصدار:",
"status_up_to_date": "✓ محدث",
"status_update_available": "⚠ يتوفر تحديث",
"status_not_installed": "✗ غير مثبت",
"status_idle": "انقر على 'التحقق من الإصدار' للبدء",
"status_checking": "🔄 جاري التحقق من الإصدار...",
"check_updates": "التحقق من الإصدار",
"check_failed": "فشل التحقق من الإصدار. يرجى التحقق من اتصال الإنترنت.",
"description": "تحقق من إصدار FFmpeg الخاص بك وقارنه بأحدث إصدار متاح.",
"guide_info": " لتثبيت أو تحديث FFmpeg، <a href='https://github.com/oop7/ffmpeg-install-guide'>انقر هنا لعرض دليل التثبيت الشامل</a>."
},
"deno": {
"setup_required": "إعداد Deno مطلوب",
"setup_description": "يتطلب YTSage Deno لتشغيل ميزات معينة.<br><br>لم يتم العثور على Deno في الدليل المحلي للتطبيق. يحتاج YTSage إلى إعداد Deno لنظام {os_name} الخاص بك.<br><br>انقر فوق 'إعداد Deno' للتنزيل والتثبيت تلقائياً.",
"setup_button": "إعداد Deno",
"downloading": "جاري تنزيل Deno...",
"extracting": "جاري استخراج Deno...",
"verifying": "جاري التحقق من Deno...",
"success": "تم تثبيت Deno بنجاح!",
"download_failed": "فشل التنزيل",
"download_error": "فشل تنزيل Deno: {error}",
"verification_failed": "فشل التحقق من SHA256. قد يكون الملف المنزل تالفاً أو تم العبث به.",
"setup_failed": "فشل إعداد Deno. قد لا تعمل بعض الميزات بشكل صحيح.",
"setup_error": "خطأ في الإعداد"
},
"deno_updater": {
"title": "فاحص ومحدث إصدار Deno",
"description": "تحقق من إصدار Deno الخاص بك وقم بالتحديث إلى أحدث إصدار.",
"current_version": "الإصدار الحالي:",
"latest_version": "أحدث إصدار:",
"status_idle": "انقر فوق 'التحقق من التحديثات' للبدء",
"status_checking": "🔄 جارٍ التحقق من التحديثات...",
"status_up_to_date": "✓ محدث",
"status_update_available": "⚠ التحديث متاح",
"status_not_installed": "✗ غير مثبت",
"check_updates": "التحقق من التحديثات",
"update_now": "تحديث Deno",
"updating": "🔄 جارٍ تحديث Deno...",
"update_success": "✅ تم تحديث Deno بنجاح!",
"update_failed": "❌ فشل التحديث: {error}",
"check_failed": "فشل التحقق من التحديثات. يرجى التحقق من اتصالك بالإنترنت."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Deutsch (German)",
"select_language": "Sprache auswählen:",
"current_language": "Aktuelle Sprache: {language}",
"restart_required": "Sprachänderungen werden nach dem Neustart der Anwendung wirksam.",
"english": "Englisch",
"spanish": "Spanisch",
"portuguese": "Portugiesisch",
"russian": "Russisch",
"chinese": "Chinesisch",
"german": "Deutsch",
"french": "Französisch",
"hindi": "Hindi",
"indonesian": "Indonesisch",
"turkish": "Türkisch",
"polish": "Polnisch",
"italian": "Italienisch",
"arabic": "Arabisch",
"japanese": "Japanisch",
"help_text": "Wählen Sie Ihre bevorzugte Sprache für die Benutzeroberfläche.",
"restart_notice": "Die Sprachänderung wird nach dem Neustart der Anwendung wirksam."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Bereit"
},
"formats": {
"show_formats": "Formate anzeigen:",
"video_format": "Videoformat:",
"audio_format": "Audioformat:",
"no_formats": "Keine Formate verfügbar",
"loading": "Lade Formate...",
"select": "Auswählen",
"quality": "Qualität",
"extension": "Erweiterung",
"resolution": "Auflösung",
"file_size": "Dateigröße",
"codec": "Codec",
"audio": "Audio",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "Audio wird zusammengeführt",
"has_audio": "✓ Hat Audio",
"audio_only": "Nur Audio",
"best_4k": "Beste (4K)",
"best_2k": "Beste (2K)",
"high_1080p": "Hoch (1080p)",
"high_720p": "Hoch (720p)",
"medium_480p": "Mittel (480p)",
"low_quality": "Niedrige Qualität",
"best_audio": "Bestes Audio",
"high_audio": "Hohes Audio",
"medium_audio": "Mittleres Audio",
"low_audio": "Niedriges Audio",
"audio_only_resolution": "Nur Audio"
},
"buttons": {
"reset": "Zurücksetzen",
"download": "Herunterladen",
"pause": "Pausieren",
"resume": "Fortsetzen",
"cancel": "Abbrechen",
"browse": "Durchsuchen",
"clear": "Löschen",
"ok": "OK",
"apply": "Anwenden",
"close": "Schließen",
"run_command": "Befehl ausführen",
"custom_command_help": "Hilfe",
"about": "Über",
"analyze": "Analysieren",
"paste_url": "URL einfügen",
"select_videos": "Videos auswählen...",
"video": "Video",
"audio_only": "Nur Audio",
"custom_options": "Benutzerdefinierte Optionen",
"trim_video": "Video schneiden",
"download_settings": "Download-Einstellungen",
"update": "yt-dlp aktualisieren",
"select_defaults": "Standard auswählen",
"select_all": "Alle auswählen",
"deselect_all": "Alle abwählen",
"open_folder": "Ordnerspeicherort öffnen",
"history": "Verlauf",
"save_playlist": "Playlist speichern unter"
},
"dialogs": {
"custom_options": "Benutzerdefinierte Optionen",
"settings": "Einstellungen",
"select_folder": "Download-Ordner auswählen",
"sponsorblock_categories": "SponsorBlock-Kategorien",
"sponsorblock_description": "Wählen Sie aus, welche Arten von Videosegmenten während des Downloads automatisch entfernt werden sollen.\nSponsorBlock verwendet von der Community übermittelte Daten, um diese Segmente zu identifizieren.",
"select_subtitles": "Untertitel auswählen",
"filter_languages_placeholder": "Sprachen filtern (z.B. en, de)...",
"no_subtitles_available": "Keine Untertitel verfügbar",
"matching": "passend",
"ytdlp_log_title": "yt-dlp-Protokoll",
"filter_playlist_placeholder": "Videos filtern..."
},
"tabs": {
"cookies": "Mit Cookies anmelden",
"custom_command": "Benutzerdefinierter Befehl",
"proxy": "Proxy",
"language": "Sprache",
"updater": "Updater"
},
"cookies": {
"help_text": "Wählen Sie aus, wie Cookies für die Anmeldung bereitgestellt werden sollen.\nDies ermöglicht das Herunterladen privater Videos und hochwertiger Audio-Dateien.",
"cookie_source": "Cookie-Quelle",
"use_cookie_file": "Cookie-Datei verwenden",
"extract_from_browser": "Aus Browser extrahieren",
"recommended": "Empfohlen",
"cookie_file": "Cookie-Datei",
"cookie_file_placeholder": "Pfad zur cookies.txt-Datei...",
"browser_selection": "Browser-Auswahl",
"browser_help": "Wählen Sie den Browser aus, aus dem Cookies extrahiert werden sollen:",
"browser_label": "Browser:",
"profile_label": "Profil:",
"profile_placeholder": "Standard",
"browser_extract_message": "Browser-Cookies werden beim Anwenden extrahiert",
"file_selected_message": "Cookie-Datei ausgewählt - Klicken Sie OK zum Anwenden",
"select_file_title": "Cookie-Datei auswählen",
"file_filter": "Cookie-Dateien (*.txt *.lwp)",
"file_selected_title": "Cookie-Datei angewendet",
"file_applied_message": "Cookie-Datei angewendet: {path}",
"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",
"active_browser": "✓ Aktiv: Browser-Cookies ({browser})",
"active_file": "✓ Aktiv: Cookie-Datei ({file})",
"none_active": "○ Keine Cookies aktiv",
"remember_settings": "Cookie-Einstellungen beim nächsten Start merken"
},
"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.",
"input_label": "yt-dlp-Argumente:",
"input_placeholder": "Geben Sie yt-dlp-Argumente hier ein...\n\nz.B.: --extract-audio --audio-format mp3",
"output_label": "Befehlsausgabe:",
"output_placeholder": "Die Befehlsausgabe wird hier erscheinen...",
"command_placeholder": "Benutzerdefinierten yt-dlp-Befehl eingeben...",
"command_help": "Verfügbare Platzhalter:\n{url} - Video-URL\n{output} - Ausgabeordner\n\nBeispiel: --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Haupt-Proxy",
"main_proxy_help": "Primärer Proxy-Server für alle Downloads. Unterstützt HTTP/HTTPS- und SOCKS5-Protokolle.",
"proxy_url_label": "Proxy-URL:",
"proxy_url_placeholder": "http://proxy:port oder socks5://proxy:port",
"proxy_examples": "Beispiele:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Geo-Umgehungs-Proxy",
"geo_proxy_help": "Sekundärer Proxy speziell zur Umgehung geografischer Beschränkungen.",
"geo_proxy_url_label": "Geo-Umgehungs-Proxy-URL:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Haupt-Proxy löschen",
"clear_geo_proxy": "Geo-Proxy löschen",
"proxy_url": "Proxy-URL",
"proxy_placeholder": "http://proxy:port oder socks5://proxy:port",
"geo_bypass": "Geo-Umgehungs-Proxy (für geografische Beschränkungen)",
"geo_bypass_placeholder": "http://proxy:port zur Umgehung von Geo-Blockierungen",
"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",
"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...",
"starting": "🚀 Download wird gestartet...",
"fetching_info": "🔍 Video-Informationen werden abgerufen...",
"preparing_streams": "🎯 Video-Streams werden vorbereitet...",
"downloading_audio": "⏬ Audio wird heruntergeladen...",
"downloading_video": "⏬ Video wird heruntergeladen...",
"downloading_subtitle": "⏬ Untertitel werden heruntergeladen...",
"downloading": "⏬ Wird heruntergeladen...",
"completed": "✅ Download abgeschlossen!",
"video_completed": "✅ Video-Download abgeschlossen!",
"audio_completed": "✅ Audio-Download abgeschlossen!",
"subtitle_completed": "✅ Untertitel-Download abgeschlossen!",
"completed_cleaning": "✅ Download abgeschlossen! Wird aufgeräumt...",
"cancelled": "Download abgebrochen",
"processing_playlist": "📋 Playlist-Daten werden verarbeitet...",
"paused": "Download pausiert",
"resumed": "Download fortgesetzt",
"merging_formats": "✨ Nachbearbeitung: Formate werden zusammengeführt...",
"removing_sponsor_segments": "✨ Nachbearbeitung: Sponsor-Segmente werden entfernt...",
"speed": "Geschwindigkeit",
"eta": "Verbleibende Zeit",
"please_enter_url": "Bitte geben Sie zuerst eine URL ein.",
"please_set_path": "Bitte setzen Sie zuerst einen Download-Pfad.",
"please_enter_url_and_path": "Bitte geben Sie eine URL ein und setzen Sie einen Download-Pfad.",
"please_select_format": "Bitte wählen Sie zuerst ein Format aus.",
"downloading_fallback": "⚡ Wird heruntergeladen..."
},
"update": {
"title": "yt-dlp aktualisieren",
"checking": "Suche nach Updates...",
"update_available": "Update verfügbar!\nAktuelle Version: {current}\nNeueste Version: {latest}",
"up_to_date": "yt-dlp ist aktuell (Version {version})",
"could_not_determine": "Versionen konnten nicht bestimmt werden.",
"error_comparing": "Fehler beim Vergleichen der Versionen: {error}",
"update_available_failed": "Update verfügbar! (Vergleich fehlgeschlagen)\nAktuell: {current}\nNeuest: {latest}",
"updating": "Wird aktualisiert...",
"initializing": "🚀 Update-Prozess wird initialisiert...",
"checking_current": "🔍 Aktuelle Installation wird überprüft...",
"found_at": "📍 yt-dlp gefunden in: {path}",
"error_getting_path": "❌ Fehler beim Abrufen des yt-dlp-Pfads: {error}",
"updating_binary": "📦 App-verwaltete yt-dlp-Binärdatei wird aktualisiert...",
"update_failed": "❌ yt-dlp-Update fehlgeschlagen. Bitte versuchen Sie es erneut oder überprüfen Sie Ihre Internetverbindung.",
"binary_updated": "✅ Binärdatei erfolgreich aktualisiert!",
"update_failed_stderr": "❌ yt-dlp-Update fehlgeschlagen: {error}",
"update_timeout": "❌ yt-dlp-Update-Zeitüberschreitung.",
"unexpected_error": "❌ Unerwarteter Fehler während des Updates: {error}",
"already_up_to_date": "✅ yt-dlp ist bereits auf dem neuesten Stand!",
"update_success": "✅ yt-dlp wurde erfolgreich aktualisiert!",
"already_latest": "yt-dlp ist auf dem neuesten Stand (Version {version})",
"network_error": "❌ Netzwerkfehler während des Updates: {error}",
"general_error": "❌ Update fehlgeschlagen: {error}",
"update_in_progress_title": "Update läuft",
"update_in_progress_message": "yt-dlp wird gerade aktualisiert. Bitte warten Sie einen Moment."
},
"about": {
"title": "Über YTSage",
"version": "Version {version}",
"description": "Moderner YouTube-Downloader mit sauberer PySide6-Oberfläche.",
"author": "Von: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Erkannt",
"missing": "✗ Fehlend",
"not_available": "Nicht verfügbar"
},
"time_range": {
"title": "Video schneiden",
"time_range_group": "Zeitbereich",
"start_time": "Startzeit (HH:MM:SS)",
"end_time": "Endzeit (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Startzeit (HH:MM:SS)",
"end_time_placeholder": "Endzeit (HH:MM:SS)",
"force_keyframes": "Keyframes an Schnittpunkten erzwingen",
"help_text": "Setzen Sie Start- und Endzeit, um das Video zu schneiden.\nLeer lassen, um das vollständige Video herunterzuladen.",
"invalid_format": "Ungültiges Zeitformat. Verwenden Sie das Format HH:MM:SS.",
"start_after_end": "Startzeit kann nicht nach der Endzeit liegen."
},
"settings": {
"title": "Download-Einstellungen",
"download_path": "Download-Pfad",
"browse": "Durchsuchen...",
"speed_limit": "Geschwindigkeitsbegrenzung",
"speed_limit_placeholder": "Keine",
"generic_mode": "Generischer Modus",
"enable_generic_mode": "Generischen Modus aktivieren (Unterstützung für Nicht-YouTube-Seiten)",
"generic_mode_help": "Ermöglicht Downloads von Dailymotion, CBC Gem und anderen von yt-dlp unterstützten Seiten.",
"auto_update_ytdlp": "yt-dlp automatisch aktualisieren",
"enable_auto_updates": "Automatische yt-dlp-Updates aktivieren",
"update_frequency": "Update-Häufigkeit:",
"check_startup": "Bei jedem Start prüfen (mindestens 1 Stunde zwischen Prüfungen)",
"check_daily": "Täglich prüfen",
"check_weekly": "Wöchentlich prüfen",
"check_updates_now": "yt-dlp aktualisieren",
"update_check_title": "Update-Prüfung",
"could_not_determine_version": "Aktuelle yt-dlp-Version konnte nicht bestimmt werden.",
"update_available_dialog": "Update verfügbar!\n\nAktuell: {current}\nNeuest: {latest}\n\nKlicken Sie auf OK, um mit dem Update fortzufahren.",
"up_to_date_dialog": "yt-dlp ist aktuell!\n\nAktuelle Version: {version}",
"error_checking_updates": "Fehler bei der Update-Prüfung: {error}",
"settings_saved_title": "Einstellungen gespeichert",
"settings_saved_message": "Auto-Update-Einstellungen wurden erfolgreich gespeichert!",
"error_title": "Fehler",
"failed_save_settings": "Speichern der Auto-Update-Einstellungen fehlgeschlagen.",
"error_saving_settings": "Fehler beim Speichern der Auto-Update-Einstellungen: {error}",
"ytdlp_channel": "yt-dlp Release-Kanal",
"ytdlp_channel_stable": "Stabil (Getestete Versionen)",
"ytdlp_channel_nightly": "Nightly (Tägliche Updates, empfohlen von yt-dlp)",
"ytdlp_channel_description": "Wählen Sie zwischen stabilen und nächtlichen Versionen. Nightly-Builds werden täglich mit den neuesten Korrekturen und Funktionen aktualisiert. Sie können jederzeit zwischen den Kanälen wechseln.",
"ytdlp_switching_channel": "Wechsle zu {channel}-Kanal...",
"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_app_updates": "Beim Start nach YTSage-Updates suchen",
"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.",
"current_status": "Aktueller Status",
"current_version_label": "Aktuelle yt-dlp-Version: Wird überprüft...",
"last_check_label": "Letzte Update-Prüfung: Nie",
"next_check_label": "Nächste Prüfung: Basierend auf Einstellungen",
"manual_check_button": "🔍 Jetzt nach Updates suchen",
"update_frequency_group": "Update-Häufigkeit",
"save_settings": "Einstellungen speichern",
"settings_saved_successfully": "✅ Einstellungen erfolgreich gespeichert!",
"error_saving": "❌ Fehler beim Speichern der Einstellungen: {error}",
"output_format_settings": "Ausgabeformat-Einstellungen",
"force_output_format": "Ausgabeformat beim Zusammenführen erzwingen",
"preferred_format": "Bevorzugtes Format:",
"force_format_help": "Wenn aktiviert, werden zusammengeführte Videos in Ihr bevorzugtes Format konvertiert. Deaktivieren Sie diese Option, damit yt-dlp automatisch entscheidet.",
"format_mp4": "MP4 (Am kompatibelsten)",
"format_webm": "WebM (Modern, offen)",
"format_mkv": "MKV (Funktionsreich)",
"audio_format_settings": "Audioformat-Einstellungen",
"force_audio_format": "Audioformat für Nur-Audio-Downloads erzwingen",
"audio_normalization": "Audio-Normalisierung (EBU R128)",
"audio_normalization_help": "Wenn aktiviert, werden Audiospuren normalisiert. Hinweis: Dies erfordert eine Neukodierung, daher muss ein bestimmtes Audioformat (wie MP3 oder M4A) erzwungen werden.",
"preferred_audio_format": "Bevorzugtes Audioformat:",
"force_audio_format_help": "Wenn aktiviert, werden Nur-Audio-Downloads in Ihr bevorzugtes Format konvertiert. Dies gilt nur beim Herunterladen von Audioformaten.",
"audio_format_best": "Beste (Keine Konvertierung)",
"audio_format_aac": "AAC (Gut zum Bearbeiten)",
"audio_format_mp3": "MP3 (Universal)",
"audio_format_flac": "FLAC (Verlustfrei)",
"audio_format_wav": "WAV (Unkomprimiert)",
"audio_format_opus": "Opus (Effizient)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "Allgemein",
"tab_format": "Format",
"tab_file": "Datei",
"concurrent_fragments": "Gleichzeitige Verbindungen",
"concurrent_fragments_help": "Anzahl der Verbindungen pro Download. Höhere Werte umgehen Drosselungen, können aber bei zu hohen Werten zu temporären Sperren führen. Standard: 1.",
"defaults_settings": "Standard-Auswahleinstellungen",
"default_video_quality": "Standard-Videoauflösung (Höhe):",
"default_subtitle_language": "Standard- Untertitelsprache(n):",
"defaults_help": "Legen Sie Ihre bevorzugte Videohöhe und Untertitelsprachen fest (kommagetrennt). Diese werden automatisch ausgewählt, falls verfügbar."
},
"main_ui": {
"url_placeholder": "YouTube-Video- oder Playlist-URL eingeben",
"url_placeholder_generic": "Video- oder Playlist-URL von jeder unterstützten Seite eingeben",
"merge_subtitles": "Untertitel zusammenführen",
"save_thumbnail": "Thumbnail speichern",
"save_description": "Beschreibung speichern",
"embed_chapters": "Kapitel einbetten",
"subtitles_selected": "{count} ausgewählt",
"all_selected": "Alle ausgewählt",
"select_videos_all": "Videos auswählen... (Alle ausgewählt)",
"please_enter_url": "Bitte geben Sie zuerst eine URL ein",
"error_title": "Fehler",
"cookie_file_selected_title": "Cookie-Datei ausgewählt",
"cookie_file_selected_message": "Cookie-Datei ausgewählt: {path}",
"browser_cookies_selected_title": "Browser-Cookies ausgewählt",
"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": "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": "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",
"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",
"sponsor_desc": "Bezahlte Werbung, bezahlte Empfehlungen und direkte Werbung",
"selfpromo": "Unbezahlte/Eigenwerbung",
"selfpromo_desc": "Unbezahlte Werbung für eigene Inhalte der Ersteller",
"interaction": "Interaktions-Erinnerung",
"interaction_desc": "Zuschauer werden gebeten zu liken, zu abonnieren oder sozialen Medien zu folgen",
"intro": "Intro",
"intro_desc": "Video-Einleitung, die übersprungen werden kann",
"outro": "Outro/Abspann-Karten",
"outro_desc": "Credits oder wenn das Video endet",
"preview": "Vorschau/Zusammenfassung",
"preview_desc": "Kurze Zusammenfassung vorheriger Videos oder Vorschau auf kommende Inhalte",
"music_offtopic": "Nicht-Musik-Bereich",
"music_offtopic_desc": "Nur für Musikvideos. Markiert Nicht-Musik-Bereiche",
"filler": "Füller-Exkurs",
"filler_desc": "Abschweifende Szenen, die nur als Füller oder für Humor hinzugefügt wurden"
},
"video_info": {
"channel": "Kanal",
"views": "👁 Aufrufe",
"likes": "👍 Gefällt mir",
"upload_date": "📅 Upload-Datum",
"duration": "⏱ Dauer",
"unknown_channel": "Unbekannter Kanal",
"unknown_date": "Unbekanntes Datum",
"unknown_title": "Unbekannter Titel"
},
"command": {
"running": "Läuft...",
"run_command": "Befehl ausführen"
},
"selection": {
"none_selected": "0 ausgewählt",
"one_selected": "1 Kategorie ausgewählt",
"count_selected": "{count} ausgewählt"
},
"status": {
"ready": "Bereit",
"file_exists": "⚠️ Datei existiert bereits",
"video_file_exists": "⚠️ Video-Datei existiert bereits",
"audio_file_exists": "⚠️ Audio-Datei existiert bereits",
"subtitle_file_exists": "⚠️ Untertitel-Datei existiert bereits",
"cancelling": "Download wird abgebrochen...",
"thumbnail_saved": "✅ Miniaturbild gespeichert: {filename}",
"thumbnail_error": "❌ Miniaturbild-Fehler: {error}",
"thumbnail_no_image": "Kein Miniaturbild zum Speichern verfügbar"
},
"errors": {
"playlist_no_videos": "Fehler: Playlist enthält keine gültigen Videos.",
"playlist_no_url": "Fehler: URL für das erste Playlist-Video konnte nicht abgerufen werden.",
"ytdlp_not_found": "Fehler: yt-dlp-Ausführbare Datei nicht gefunden. Bitte installieren Sie zuerst yt-dlp.",
"ytdlp_not_found_path": "Fehler: yt-dlp-Ausführbare Datei nicht gefunden. Dies könnte auf eine fehlerhafte Installation oder ein PATH-Problem zurückzuführen sein.",
"no_data_returned": "Fehler: Keine Daten von yt-dlp zurückgegeben",
"no_format_info": "Fehler: Keine Formatinformationen verfügbar.",
"analysis_timeout": "Fehler: Analyse-Zeitüberschreitung. Bitte versuchen Sie es erneut.",
"invalid_speed_limit": "❌ Fehler: Ungültiger Geschwindigkeitsbegrenzungswert in den Einstellungen.",
"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}",
"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}",
"private_video": "Dieses Video ist möglicherweise privat. Bitte verwenden Sie Cookies aus den benutzerdefinierten Optionen."
},
"update_dialog": {
"title": "Update verfügbar",
"new_version_available": "Eine neue Version von YTSage ist verfügbar!",
"current_version_label": "Aktuelle Version:",
"latest_version_label": "Neueste Version:",
"changelog": "Änderungsprotokoll",
"download_update": "Update herunterladen",
"remind_later": "Später erinnern"
},
"playlist": {
"unknown": "Unbekannte Playlist",
"total_videos": "Gesamtanzahl Videos: {count}",
"display_format": "Playlist: {title} | {count} Videos",
"select_videos_title": "Playlist-Videos auswählen",
"save_as": "Playlist speichern unter",
"save_success_title": "Erfolg",
"saved_successfully": "Playlist erfolgreich gespeichert.",
"save_error_title": "Fehler beim Speichern",
"no_videos_to_save": "Keine Playlist-Einträge gesammelt!",
"save_error_msg": "Fehler beim Speichern der Playlist."
},
"subtitle_selection": {
"count_selected": "{count} ausgewählt"
},
"file_exists_dialog": {
"title": "Datei existiert bereits",
"message": "Die Datei existiert bereits:\n{filename}",
"info": "Dieses Video wurde bereits heruntergeladen."
},
"auto_update": {
"last_check_never": "Letzte Aktualisierungsprüfung: Nie",
"last_check": "Letzte Aktualisierungsprüfung: {time}",
"next_check_disabled": "Nächste Prüfung: Deaktiviert",
"next_check_startup": "Nächste Prüfung: Beim Start",
"next_check_overdue": "Nächste Prüfung: Jetzt (überfällig)",
"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",
"current_version": "Aktuelle yt-dlp-Version: {version}"
},
"url_validation": {
"empty_url": "URL kann nicht leer sein",
"invalid_format": "Ungültiges URL-Format",
"invalid_scheme": "URL muss mit http:// oder https:// beginnen",
"missing_domain": "Ungültige URL: Domain-Name fehlt",
"unsupported_platform": "YTSage unterstützt nur YouTube- und YouTube Music-URLs.\nDie Domain '{domain}' wird nicht unterstützt.",
"invalid_youtu_be": "Ungültige youtu.be-URL: Video-ID fehlt"
},
"ytdlp_errors": {
"private_video": "Dies ist ein privates Video. Sie können es herunterladen, indem Sie sich mit Cookies in Ihr Konto einloggen.\nGehen Sie zu 'Benutzerdefinierte Optionen' → 'Mit Cookies anmelden' → 'Cookies aus Browser extrahieren', um sich zu authentifizieren.",
"age_restricted": "Dieses Video ist altersbeschränkt. Sie müssen angemeldet sein, um darauf zuzugreifen.\nVerwenden Sie 'Benutzerdefinierte Optionen' → 'Mit Cookies anmelden', um sich mit Ihrem Konto zu authentifizieren.",
"geo_blocked": "Dieses Video ist in Ihrer Region nicht verfügbar (geografisch gesperrt).\nSie benötigen möglicherweise ein VPN oder das Video ist in Ihrem Land eingeschränkt.",
"video_unavailable": "Dieses Video wurde entfernt oder ist nicht mehr verfügbar.\nDas Video wurde möglicherweise vom Uploader gelöscht oder wegen Richtlinienverstößen entfernt.",
"live_stream": "Dies ist ein Live-Stream, der während der Aktivität nicht heruntergeladen werden kann.\nWarten Sie, bis der Stream endet, und versuchen Sie dann, die archivierte Version herunterzuladen.",
"playlist_error": "Zugriff auf diese Playlist nicht möglich. Sie ist möglicherweise privat, gelöscht oder leer.\nÜberprüfen Sie, ob die Playlist existiert und öffentlich zugänglich ist.",
"network_error": "Netzwerkverbindungsfehler. Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.\nWenn das Problem weiterhin besteht, ist der Videoserver möglicherweise vorübergehend nicht verfügbar.",
"invalid_url": "Ungültige oder nicht unterstützte URL. Bitte überprüfen Sie den Link und versuchen Sie es erneut.\nStellen Sie sicher, dass Sie eine gültige YouTube-, Vimeo- oder andere unterstützte Plattform-URL verwenden.",
"premium_content": "Dieser Inhalt erfordert YouTube Premium oder eine Kanalmitgliedschaft.\nSie müssen mit einem Konto angemeldet sein, das Zugriff auf diesen Inhalt hat.",
"copyright_blocked": "Dieses Video ist aufgrund von Urheberrechtsansprüchen gesperrt.\nDer Inhaltsinhaber hat den Zugriff auf dieses Video eingeschränkt.",
"extraction_failed": "Fehler beim Extrahieren von Videoinformationen. Dies könnte ein vorübergehendes Problem sein.\nBitte versuchen Sie es in ein paar Minuten erneut oder überprüfen Sie, ob der Videolink korrekt ist.",
"generic_error": "Videoinformationen konnten nicht extrahiert werden. Bitte überprüfen Sie Ihren Link.\nTechnische Details: {error}"
},
"history": {
"title": "Download-Verlauf",
"clear_all": "Alles Löschen",
"clear_confirm_title": "Verlauf Löschen?",
"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",
"remove": "Entfernen",
"file_not_found": "Datei nicht gefunden",
"file_not_found_message": "Die Datei wurde verschoben oder gelöscht:\n{path}",
"downloaded_on": "Geladen: {date}",
"file_size": "Größe: {size}",
"audio_download": "Audio",
"video_download": "Video",
"remove_confirm_title": "Entfernen?",
"remove_confirm_message": "Aus Verlauf entfernen?\n\n{title}",
"item_removed": "Entfernt",
"history_cleared": "Verlauf gelöscht",
"entries_count": "{count} Downloads",
"one_entry": "1 Download",
"redownload_confirm_title": "Erneut Laden?",
"redownload_confirm_message": "Erneut herunterladen?\n\n{title}",
"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",
"current_version": "Aktuelle Version:",
"latest_version": "Neueste Version:",
"status_up_to_date": "✓ Auf dem neuesten Stand",
"status_update_available": "⚠ Update verfügbar",
"status_not_installed": "✗ Nicht installiert",
"status_idle": "Klicken Sie auf 'Version prüfen' um zu beginnen",
"status_checking": "🔄 Version wird geprüft...",
"check_updates": "Version prüfen",
"check_failed": "Versionprüfung fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.",
"description": "Überprüfen Sie Ihre FFmpeg-Version und vergleichen Sie sie mit der neuesten verfügbaren Version.",
"guide_info": " Um FFmpeg zu installieren oder zu aktualisieren, <a href='https://github.com/oop7/ffmpeg-install-guide'>klicken Sie hier, um unseren umfassenden Installationsleitfaden anzuzeigen</a>."
},
"deno": {
"setup_required": "Deno-Einrichtung erforderlich",
"setup_description": "YTSage benötigt Deno, um bestimmte Funktionen auszuführen.<br><br>Deno wurde im lokalen Verzeichnis der App nicht gefunden. YTSage muss Deno für Ihr {os_name}-System einrichten.<br><br>Klicken Sie auf 'Deno einrichten', um es automatisch herunterzuladen und zu installieren.",
"setup_button": "Deno einrichten",
"downloading": "Deno wird heruntergeladen...",
"extracting": "Deno wird extrahiert...",
"verifying": "Deno wird überprüft...",
"success": "Deno wurde erfolgreich installiert!",
"download_failed": "Download fehlgeschlagen",
"download_error": "Deno konnte nicht heruntergeladen werden: {error}",
"verification_failed": "SHA256-Überprüfung fehlgeschlagen. Die heruntergeladene Datei ist möglicherweise beschädigt oder manipuliert.",
"setup_failed": "Deno konnte nicht eingerichtet werden. Einige Funktionen funktionieren möglicherweise nicht richtig.",
"setup_error": "Einrichtungsfehler"
},
"deno_updater": {
"title": "Deno Versionsprüfer & Updater",
"description": "Überprüfen Sie Ihre Deno-Version und aktualisieren Sie auf die neueste Version.",
"current_version": "Aktuelle Version:",
"latest_version": "Neueste Version:",
"status_idle": "Klicken Sie auf 'Nach Updates suchen', um zu beginnen",
"status_checking": "🔄 Suche nach Updates...",
"status_up_to_date": "✓ Aktuell",
"status_update_available": "⚠ Update verfügbar",
"status_not_installed": "✗ Nicht installiert",
"check_updates": "Nach Updates suchen",
"update_now": "Deno aktualisieren",
"updating": "🔄 Deno wird aktualisiert...",
"update_success": "✅ Deno wurde erfolgreich aktualisiert!",
"update_failed": "❌ Update fehlgeschlagen: {error}",
"check_failed": "Suche nach Updates fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "English",
"select_language": "Select language:",
"current_language": "Current language: {language}",
"restart_required": "Language changes will take effect after restarting the application.",
"english": "English",
"spanish": "Español (Spanish)",
"portuguese": "Português (Portuguese)",
"russian": "Русский (Russian)",
"chinese": "中文 (简体) (Chinese Simplified)",
"german": "Deutsch (German)",
"french": "Français (French)",
"hindi": "हिन्दी (Hindi)",
"indonesian": "Bahasa Indonesia (Indonesian)",
"turkish": "Türkçe (Turkish)",
"polish": "Polski (Polish)",
"italian": "Italiano (Italian)",
"arabic": "العربية (Arabic)",
"japanese": "日本語 (Japanese)",
"help_text": "Select your preferred language for the interface.",
"restart_notice": "Language change will take effect after restarting the application."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Ready"
},
"formats": {
"show_formats": "Show Formats:",
"video_format": "Video Format:",
"audio_format": "Audio Format:",
"no_formats": "No formats available",
"loading": "Loading formats...",
"select": "Select",
"quality": "Quality",
"extension": "Extension",
"resolution": "Resolution",
"file_size": "File Size",
"codec": "Codec",
"audio": "Audio",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "Will merge audio",
"has_audio": "✓ Has Audio",
"audio_only": "Audio Only",
"best_4k": "Best (4K)",
"best_2k": "Best (2K)",
"high_1080p": "High (1080p)",
"high_720p": "High (720p)",
"medium_480p": "Medium (480p)",
"low_quality": "Low Quality",
"best_audio": "Best Audio",
"high_audio": "High Audio",
"medium_audio": "Medium Audio",
"low_audio": "Low Audio",
"audio_only_resolution": "Audio only"
},
"buttons": {
"download": "Download",
"pause": "Pause",
"resume": "Resume",
"cancel": "Cancel",
"browse": "Browse",
"clear": "Clear",
"ok": "OK",
"apply": "Apply",
"close": "Close",
"run_command": "Run Command",
"custom_command_help": "Help",
"about": "About",
"history": "History",
"analyze": "Analyze",
"paste_url": "Paste URL",
"select_videos": "Select Videos...",
"video": "Video",
"audio_only": "Audio Only",
"custom_options": "Custom Options",
"trim_video": "Trim Video",
"download_settings": "Download Settings",
"update": "Update yt-dlp",
"select_defaults": "Select Defaults",
"select_all": "Select All",
"deselect_all": "Deselect All",
"open_folder": "Open folder location",
"save_playlist": "Save Playlist As",
"reset": "Reset"
},
"dialogs": {
"custom_options": "Custom Options",
"settings": "Settings",
"select_folder": "Select Download Folder",
"sponsorblock_categories": "SponsorBlock Categories",
"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",
"ytdlp_log_title": "yt-dlp Log"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language",
"updater": "Updater"
},
"cookies": {
"help_text": "Choose how to provide cookies for logging in.\nThis allows downloading of private videos and premium quality audio.",
"cookie_source": "Cookie Source",
"use_cookie_file": "Use cookie file",
"extract_from_browser": "Extract from browser",
"recommended": "Recommended",
"cookie_file": "Cookie File",
"cookie_file_placeholder": "Path to cookies.txt file...",
"browser_selection": "Browser Selection",
"browser_help": "Select the browser to extract cookies from:",
"browser_label": "Browser:",
"profile_label": "Profile:",
"profile_placeholder": "default",
"browser_extract_message": "Browser cookies will be extracted when applied",
"file_selected_message": "Cookie file selected - Click OK to apply",
"select_file_title": "Select Cookie File",
"file_filter": "Cookies files (*.txt *.lwp)",
"file_selected_title": "Cookie File Applied",
"file_applied_message": "Cookie file applied: {path}",
"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",
"active_browser": "✓ Active: Browser cookies ({browser})",
"active_file": "✓ Active: Cookie file ({file})",
"none_active": "○ No cookies active",
"remember_settings": "Remember cookie settings on next startup"
},
"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.",
"input_label": "yt-dlp Arguments:",
"input_placeholder": "Enter yt-dlp arguments here...\n\ne.g. --extract-audio --audio-format mp3",
"output_label": "Command Output:",
"output_placeholder": "Command output will appear here...",
"command_placeholder": "Enter custom yt-dlp command...",
"command_help": "Available placeholders:\n{url} - Video URL\n{output} - Output directory\n\nExample: --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Main Proxy",
"main_proxy_help": "Primary proxy server for all downloads. Supports HTTP/HTTPS and SOCKS5 protocols.",
"proxy_url_label": "Proxy URL:",
"proxy_url_placeholder": "http://proxy:port or socks5://proxy:port",
"proxy_examples": "Examples:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Geo-bypass Proxy",
"geo_proxy_help": "Secondary proxy specifically for bypassing geographic restrictions.",
"geo_proxy_url_label": "Geo-bypass Proxy URL:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Clear Main Proxy",
"clear_geo_proxy": "Clear Geo Proxy",
"proxy_url": "Proxy URL",
"proxy_placeholder": "http://proxy:port or socks5://proxy:port",
"geo_bypass": "Geo-bypass proxy (for geographic restrictions)",
"geo_bypass_placeholder": "http://proxy:port for bypassing geo-blocks",
"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",
"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...",
"starting": "🚀 Starting download...",
"fetching_info": "🔍 Fetching video information...",
"preparing_streams": "🎯 Preparing video streams...",
"downloading_audio": "⏬ Downloading audio...",
"downloading_video": "⏬ Downloading video...",
"downloading_subtitle": "⏬ Downloading subtitle...",
"downloading": "⏬ Downloading...",
"completed": "✅ Download completed!",
"video_completed": "✅ Video download completed!",
"audio_completed": "✅ Audio download completed!",
"subtitle_completed": "✅ Subtitle download completed!",
"completed_cleaning": "✅ Download completed! Cleaning up...",
"cancelled": "Download cancelled",
"processing_playlist": "📋 Processing playlist data...",
"paused": "Download paused",
"resumed": "Download resumed",
"merging_formats": "✨ Post-processing: Merging formats...",
"removing_sponsor_segments": "✨ Post-processing: Removing sponsor segments...",
"speed": "Speed",
"eta": "ETA",
"please_enter_url": "Please enter a URL first.",
"please_set_path": "Please set a download path first.",
"please_enter_url_and_path": "Please enter a URL and set download path.",
"please_select_format": "Please select a format first.",
"downloading_fallback": "⚡ Downloading..."
},
"update": {
"title": "Update yt-dlp",
"checking": "Checking for updates...",
"update_available": "Update available!\nCurrent version: {current}\nLatest version: {latest}",
"up_to_date": "yt-dlp is up to date (version {version})",
"already_up_to_date": "✅ yt-dlp is already up to date!",
"could_not_determine": "Could not determine versions.",
"error_comparing": "Error comparing versions: {error}",
"update_available_failed": "Update available! (Comparison failed)\nCurrent: {current}\nLatest: {latest}",
"updating": "Updating...",
"initializing": "🚀 Initializing update process...",
"checking_current": "🔍 Checking current installation...",
"found_at": "📍 Found yt-dlp at: {path}",
"error_getting_path": "❌ Error getting yt-dlp path: {error}",
"updating_binary": "📦 Updating app-managed yt-dlp binary...",
"update_failed": "❌ Failed to update yt-dlp. Please try again or check your internet connection.",
"binary_updated": "✅ Binary successfully updated!",
"update_failed_stderr": "❌ yt-dlp update failed: {error}",
"update_timeout": "❌ yt-dlp update timed out.",
"unexpected_error": "❌ Unexpected error during update: {error}",
"update_success": "✅ yt-dlp has been successfully updated!",
"already_latest": "yt-dlp is up to date (version {version})",
"network_error": "❌ Network error during update: {error}",
"general_error": "❌ Update failed: {error}",
"update_in_progress_title": "Update in Progress",
"update_in_progress_message": "yt-dlp is currently updating. Please wait a moment."
},
"about": {
"title": "About YTSage",
"version": "Version {version}",
"description": "Modern YouTube downloader with clean PySide6 interface.",
"author": "By: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Detected",
"missing": "✗ Missing",
"not_available": "Not Available"
},
"time_range": {
"title": "Trim Video",
"time_range_group": "Time Range",
"start_time": "Start Time (HH:MM:SS)",
"end_time": "End Time (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Start time (HH:MM:SS)",
"end_time_placeholder": "End time (HH:MM:SS)",
"force_keyframes": "Force keyframes at cut points",
"help_text": "Set the start and end times to trim the video.\nLeave empty to download the full video.",
"invalid_format": "Invalid time format. Use HH:MM:SS format.",
"start_after_end": "Start time cannot be after end time."
},
"settings": {
"title": "Download Settings",
"download_path": "Download Path",
"browse": "Browse...",
"speed_limit": "Speed Limit",
"speed_limit_placeholder": "None",
"generic_mode": "Generic Mode",
"enable_generic_mode": "Enable Generic Mode (support non-YouTube sites)",
"generic_mode_help": "Allows downloading from Dailymotion, CBC Gem, and other sites supported by yt-dlp.",
"auto_update_ytdlp": "Auto-Update yt-dlp",
"enable_auto_updates": "Enable automatic yt-dlp updates",
"update_frequency": "Update frequency:",
"check_startup": "Check on every startup (minimum 1 hour between checks)",
"check_daily": "Check daily",
"check_weekly": "Check weekly",
"check_updates_now": "Update yt-dlp",
"update_check_title": "Update Check",
"could_not_determine_version": "Could not determine current yt-dlp version.",
"update_available_dialog": "Update available!\n\nCurrent: {current}\nLatest: {latest}\n\nClick OK to proceed with the update.",
"up_to_date_dialog": "yt-dlp is up to date!\n\nCurrent version: {version}",
"error_checking_updates": "Error checking for updates: {error}",
"settings_saved_title": "Settings Saved",
"settings_saved_message": "Auto-update settings have been saved successfully!",
"error_title": "Error",
"failed_save_settings": "Failed to save auto-update settings.",
"error_saving_settings": "Error saving auto-update settings: {error}",
"ytdlp_channel": "yt-dlp Release Channel",
"ytdlp_channel_stable": "Stable (Tested releases)",
"ytdlp_channel_nightly": "Nightly (Daily updates, recommended by yt-dlp)",
"ytdlp_channel_description": "Choose between stable and nightly releases. Nightly builds are updated daily with the latest fixes and features. You can switch channels at any time.",
"ytdlp_switching_channel": "Switching to {channel} channel...",
"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_app_updates": "Check for YTSage updates on startup",
"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.",
"current_status": "Current Status",
"current_version_label": "Current yt-dlp version: Checking...",
"last_check_label": "Last update check: Never",
"next_check_label": "Next check: Based on settings",
"manual_check_button": "🔍 Check for Updates Now",
"update_frequency_group": "Update Frequency",
"save_settings": "Save Settings",
"settings_saved_successfully": "✅ Settings saved successfully!",
"error_saving": "❌ Error saving settings: {error}",
"output_format_settings": "Output Format Settings",
"force_output_format": "Force output format when merging",
"preferred_format": "Preferred format:",
"force_format_help": "When enabled, merged videos will be converted to your preferred format. Disable to let yt-dlp decide automatically.",
"format_mp4": "MP4 (Most compatible)",
"format_webm": "WebM (Modern, open)",
"format_mkv": "MKV (Feature-rich)",
"audio_format_settings": "Audio Format Settings",
"force_audio_format": "Force audio format for audio-only downloads",
"audio_normalization": "Audio Normalization (EBU R128)",
"audio_normalization_help": "When enabled, audio tracks will be normalized. Note: This requires re-encoding, so a specific audio format (like MP3 or M4A) must be forced.",
"filename_format": "Output Filename Format",
"filename_format_help": "Available variables: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s, %(playlist_index)s. Standard yt-dlp output template syntax is supported.",
"defaults_settings": "Default Selection Settings",
"default_video_quality": "Default Video Resolution (Height):",
"default_subtitle_language": "Default Subtitle Language(s):",
"defaults_help": "Set your preferred video height and subtitle languages (comma-separated). They will be auto-selected if available.",
"concurrent_fragments": "Concurrent Connections",
"concurrent_fragments_help": "Number of connections per download. Higher values bypass throttling but may cause temporary blocks if set too high. Default: 1.",
"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)",
"audio_format_aac": "AAC (Good for editing)",
"audio_format_mp3": "MP3 (Universal)",
"audio_format_flac": "FLAC (Lossless)",
"audio_format_wav": "WAV (Uncompressed)",
"audio_format_opus": "Opus (Efficient)",
"audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Open)",
"tab_general": "General",
"tab_format": "Format",
"tab_file": "File"
},
"main_ui": {
"url_placeholder": "Enter YouTube video or playlist URL",
"url_placeholder_generic": "Enter video or playlist URL from any supported site",
"merge_subtitles": "Merge Subtitles",
"save_thumbnail": "Save Thumbnail",
"save_description": "Save Description",
"embed_chapters": "Embed Chapters",
"subtitles_selected": "{count} selected",
"all_selected": "All selected",
"select_videos_all": "Select Videos... (All selected)",
"please_enter_url": "Please enter a URL first",
"error_title": "Error",
"cookie_file_selected_title": "Cookie File Selected",
"cookie_file_selected_message": "Cookie file selected: {path}",
"browser_cookies_selected_title": "Browser Cookies Selected",
"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": "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": "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",
"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",
"sponsor_desc": "Paid promotion, paid referrals and direct advertisements",
"selfpromo": "Unpaid/Self Promotion",
"selfpromo_desc": "Unpaid promotion of creators' own content",
"interaction": "Interaction Reminder",
"interaction_desc": "Asking viewers to like, subscribe, or follow social media",
"intro": "Intro",
"intro_desc": "Video introduction that can be skipped",
"outro": "Outro/End Cards",
"outro_desc": "Credits or when the video ends",
"preview": "Preview/Recap",
"preview_desc": "Quick recap of previous videos or preview of what's coming up",
"music_offtopic": "Non-Music Section",
"music_offtopic_desc": "Only for music videos. Marks non-music sections",
"filler": "Filler Tangent",
"filler_desc": "Tangential scenes added only for filler or humor"
},
"video_info": {
"channel": "Channel",
"views": "👁 Views",
"likes": "👍 Likes",
"upload_date": "📅 Upload date",
"duration": "⏱ Duration",
"unknown_channel": "Unknown channel",
"unknown_date": "Unknown date",
"unknown_title": "Unknown title"
},
"command": {
"running": "Running...",
"run_command": "Run Command"
},
"selection": {
"none_selected": "0 selected",
"one_selected": "1 category selected",
"count_selected": "{count} selected"
},
"status": {
"ready": "Ready",
"file_exists": "⚠️ File already exists",
"video_file_exists": "⚠️ Video file already exists",
"audio_file_exists": "⚠️ Audio file already exists",
"subtitle_file_exists": "⚠️ Subtitle file already exists",
"cancelling": "Cancelling download...",
"thumbnail_saved": "✅ Thumbnail saved: {filename}",
"thumbnail_error": "❌ Thumbnail error: {error}",
"thumbnail_no_image": "No thumbnail available to save"
},
"errors": {
"playlist_no_videos": "Error: Playlist contains no valid videos.",
"playlist_no_url": "Error: Could not get URL for the first playlist video.",
"ytdlp_not_found": "Error: yt-dlp executable not found. Please install yt-dlp first.",
"ytdlp_not_found_path": "Error: yt-dlp executable not found. This could be due to improper installation or a PATH issue.",
"no_data_returned": "Error: No data returned from yt-dlp",
"no_format_info": "Error: No format information available.",
"analysis_timeout": "Error: Analysis timed out. Please try again.",
"invalid_speed_limit": "❌ Error: Invalid speed limit value set in settings.",
"ytdlp_failed": "Error: yt-dlp failed: {error}",
"private_video": "This video might be private. Please use cookies from the custom options.",
"parse_failed": "Error: Failed to parse yt-dlp output: {error}",
"analysis_failed": "Error: Analysis failed: {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",
"new_version_available": "A new version of YTSage is available!",
"current_version_label": "Current version:",
"latest_version_label": "Latest version:",
"changelog": "Changelog",
"download_update": "Download Update",
"remind_later": "Remind Me Later"
},
"playlist": {
"unknown": "Unknown Playlist",
"total_videos": "Total Videos: {count}",
"display_format": "Playlist: {title} | {count} videos",
"select_videos_title": "Select Playlist Videos",
"save_as": "Save Playlist As",
"save_success_title": "Success",
"saved_successfully": "Playlist saved successfully.",
"save_error_title": "Save Error",
"no_videos_to_save": "No playlist entries gathered!",
"save_error_msg": "Failed to save playlist."
},
"subtitle_selection": {
"count_selected": "{count} selected"
},
"file_exists_dialog": {
"title": "File Already Exists",
"message": "The file already exists:\n{filename}",
"info": "This video has already been downloaded."
},
"auto_update": {
"last_check_never": "Last update check: Never",
"last_check": "Last update check: {time}",
"next_check_disabled": "Next check: Disabled",
"next_check_startup": "Next check: On next startup",
"next_check_overdue": "Next check: Now (overdue)",
"next_check": "Next check: {time}",
"next_check_error": "Next check: Error calculating",
"checking": "🔄 Checking...",
"check_now": "🔍 Check for Updates Now",
"current_version": "Current yt-dlp version: {version}"
},
"url_validation": {
"empty_url": "URL cannot be empty",
"invalid_format": "Invalid URL format",
"invalid_scheme": "URL must start with http:// or https://",
"missing_domain": "Invalid URL: missing domain name",
"unsupported_platform": "YTSage only supports YouTube and YouTube Music URLs.\nThe domain '{domain}' is not supported.",
"invalid_youtu_be": "Invalid youtu.be URL: missing video ID"
},
"ytdlp_errors": {
"private_video": "This is a private video. You can download it by logging into your account using cookies.\nGo to 'Custom Options' → 'Login with Cookies' → 'Extract cookies from browser' to authenticate.",
"age_restricted": "This video is age-restricted. You need to be logged in to access it.\nUse 'Custom Options' → 'Login with Cookies' to authenticate with your account.",
"geo_blocked": "This video is not available in your region (geo-blocked).\nYou may need to use a VPN or the video might be restricted in your country.",
"video_unavailable": "This video has been removed or is no longer available.\nThe video may have been deleted by the uploader or removed due to policy violations.",
"live_stream": "This is a live stream that cannot be downloaded while active.\nWait for the stream to end, then try downloading the archived version.",
"playlist_error": "Unable to access this playlist. It may be private, deleted, or empty.\nCheck if the playlist exists and is publicly accessible.",
"network_error": "Network connection error. Please check your internet connection and try again.\nIf the problem persists, the video server might be temporarily unavailable.",
"invalid_url": "Invalid or unsupported URL. Please check the link and try again.\nMake sure you're using a valid YouTube, Vimeo, or other supported platform URL.",
"premium_content": "This content requires YouTube Premium or channel membership.\nYou need to be logged in with an account that has access to this content.",
"copyright_blocked": "This video is blocked due to copyright claims.\nThe content owner has restricted access to this video.",
"extraction_failed": "Failed to extract video information. This might be a temporary issue.\nPlease try again in a few minutes, or check if the video link is correct.",
"generic_error": "Could not extract video information. Please check your link.\nTechnical details: {error}"
},
"history": {
"title": "Download History",
"clear_all": "Clear All History",
"clear_confirm_title": "Clear History?",
"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",
"remove": "Remove from History",
"file_not_found": "File not found at original location",
"file_not_found_message": "The file was moved or deleted:\n{path}",
"downloaded_on": "Downloaded on {date}",
"file_size": "Size: {size}",
"audio_download": "Audio",
"video_download": "Video",
"remove_confirm_title": "Remove from History?",
"remove_confirm_message": "Remove this item from history?\n\n{title}",
"item_removed": "Item removed from history",
"history_cleared": "History cleared",
"entries_count": "{count} downloads",
"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",
"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",
"current_version": "Current Version:",
"latest_version": "Latest Version:",
"status_up_to_date": "✓ Up to date",
"status_update_available": "⚠ Update available",
"status_not_installed": "✗ Not installed",
"status_idle": "Click 'Check Version' to get started",
"status_checking": "🔄 Checking version...",
"check_updates": "Check Version",
"check_failed": "Failed to check version. Please check your internet connection.",
"description": "Check your FFmpeg version and compare it with the latest available version.",
"guide_info": " To install or update FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>click here to view our comprehensive installation guide</a>."
},
"deno": {
"setup_required": "Deno Setup Required",
"setup_description": "YTSage requires Deno to run certain features.<br><br>Deno was not found in the app's local directory. YTSage needs to set up Deno for your {os_name} system.<br><br>Click 'Setup Deno' to download and install automatically.",
"setup_button": "Setup Deno",
"downloading": "Downloading Deno...",
"extracting": "Extracting Deno...",
"verifying": "Verifying Deno...",
"success": "Deno was successfully installed!",
"download_failed": "Download Failed",
"download_error": "Failed to download Deno: {error}",
"verification_failed": "SHA256 verification failed. The downloaded file may be corrupted or tampered with.",
"setup_failed": "Failed to set up Deno. Some features may not work correctly.",
"setup_error": "Setup Error"
},
"deno_updater": {
"title": "Deno Version Checker & Updater",
"description": "Check your Deno version and update to the latest release.",
"current_version": "Current Version:",
"latest_version": "Latest Version:",
"status_idle": "Click 'Check for Updates' to get started",
"status_checking": "🔄 Checking for updates...",
"status_up_to_date": "✓ Up to date",
"status_update_available": "⚠ Update available",
"status_not_installed": "✗ Not installed",
"check_updates": "Check for Updates",
"update_now": "Update Deno",
"updating": "🔄 Updating Deno...",
"update_success": "✅ Deno has been successfully updated!",
"update_failed": "❌ Update failed: {error}",
"check_failed": "Failed to check for updates. Please check your internet connection."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Español (Spanish)",
"select_language": "Seleccionar idioma:",
"current_language": "Idioma actual: {language}",
"restart_required": "Los cambios de idioma tendrán efecto después de reiniciar la aplicación.",
"english": "Inglés",
"spanish": "Español (Spanish)",
"portuguese": "Português (Portuguese)",
"russian": "Русский (Russian)",
"chinese": "中文 (简体) (Chinese Simplified)",
"german": "Deutsch (German)",
"french": "Français (French)",
"hindi": "Hindi",
"indonesian": "Indonesio",
"turkish": "Turco",
"polish": "Polaco",
"italian": "Italiano",
"arabic": "Árabe",
"japanese": "Japonés",
"help_text": "Seleccione su idioma preferido para la interfaz.",
"restart_notice": "El cambio de idioma tendrá efecto después de reiniciar la aplicación."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Listo"
},
"buttons": {
"reset": "Restablecer",
"download": "Descargar",
"pause": "Pausar",
"resume": "Reanudar",
"cancel": "Cancelar",
"browse": "Examinar",
"clear": "Limpiar",
"ok": "Aceptar",
"apply": "Aplicar",
"close": "Cerrar",
"run_command": "Ejecutar Comando",
"about": "Acerca de",
"update": "Actualizar",
"analyze": "Analizar",
"paste_url": "Pegar URL",
"select_videos": "Seleccionar Videos...",
"video": "Video",
"audio_only": "Solo Audio",
"custom_options": "Opciones Personalizadas",
"trim_video": "Recortar Video",
"download_settings": "Configuración de Descarga",
"select_defaults": "Seleccionar Predeterminados",
"select_all": "Seleccionar Todos",
"deselect_all": "Deseleccionar Todos",
"open_folder": "Abrir ubicación de carpeta",
"history": "Historial",
"save_playlist": "Guardar playlist como",
"custom_command_help": "Ayuda"
},
"dialogs": {
"custom_options": "Opciones Personalizadas",
"settings": "Configuración",
"select_folder": "Seleccionar Carpeta de Descarga",
"sponsorblock_categories": "Categorías SponsorBlock",
"sponsorblock_description": "Selecciona qué tipos de segmentos de video se eliminarán automáticamente durante la descarga.\nSponsorBlock utiliza datos enviados por la comunidad para identificar estos segmentos.",
"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",
"ytdlp_log_title": "Registro de yt-dlp",
"filter_playlist_placeholder": "Filtrar videos..."
},
"tabs": {
"cookies": "Iniciar sesión con Cookies",
"custom_command": "Comando Personalizado",
"proxy": "Proxy",
"language": "Idioma",
"updater": "Actualizador"
},
"cookies": {
"help_text": "Elige cómo proporcionar cookies para iniciar sesión.\nEsto permite la descarga de videos privados y audio de calidad premium.",
"cookie_source": "Origen de Cookie",
"use_cookie_file": "Usar archivo de cookie (formato Netscape)",
"extract_from_browser": "Extraer cookies del navegador",
"recommended": "Recomendado",
"cookie_file": "Archivo de Cookie",
"cookie_file_placeholder": "Ruta al archivo de cookies (formato Netscape)",
"browser_selection": "Selección de Navegador",
"browser_help": "Selecciona el navegador del cual extraer cookies. Asegúrate de que el navegador esté cerrado antes de la extracción.",
"browser_label": "Navegador:",
"profile_label": "Perfil (opcional):",
"profile_placeholder": "Nombre o ruta del perfil (dejar vacío para el predeterminado)",
"browser_extract_message": "Las cookies del navegador se extraerán al aplicar",
"file_selected_message": "Archivo de cookies seleccionado - Haz clic en Aceptar para aplicar",
"select_file_title": "Seleccionar Archivo de Cookies",
"file_filter": "Archivos de cookies (*.txt *.lwp)",
"file_selected_title": "Archivo de Cookies Aplicado",
"file_applied_message": "Archivo de cookies aplicado: {path}",
"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",
"active_browser": "✓ Activas: cookies del navegador ({browser})",
"active_file": "✓ Activo: archivo de cookies ({file})",
"none_active": "○ No hay cookies activas",
"remember_settings": "Recordar configuración de cookies en el próximo inicio"
},
"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.",
"input_label": "Argumentos de yt-dlp:",
"input_placeholder": "Ingresa argumentos de yt-dlp aquí...\n\nej. --extract-audio --audio-format mp3",
"output_label": "Salida del Comando:",
"output_placeholder": "La salida del comando aparecerá aquí...",
"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}",
"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": "==================================================",
"command_placeholder": "Enter custom yt-dlp command...",
"command_help": "Available placeholders:\n{url} - Video URL\n{output} - Output directory\n\nExample: --write-info-json --write-thumbnail"
},
"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.",
"main_proxy": "Proxy Principal",
"main_proxy_help": "Usar el proxy HTTP/HTTPS/SOCKS especificado para todas las conexiones.",
"proxy_url_label": "URL del Proxy:",
"proxy_url_placeholder": "ej., http://proxy.example.com:8080 o socks5://user:pass@127.0.0.1:1080",
"proxy_examples": "Ejemplos: http://proxy.com:8080, https://proxy.com:8080, socks5://127.0.0.1:1080",
"geo_proxy": "Proxy de Geo-verificación",
"geo_proxy_help": "Usar este proxy para verificar dirección IP para sitios con restricciones geográficas. El proxy principal (si está configurado) se usa para la descarga real.",
"geo_proxy_url_label": "URL del Geo Proxy:",
"geo_proxy_url_placeholder": "ej., http://us-proxy.example.com:8080",
"clear_main_proxy": "Limpiar Proxy Principal",
"clear_geo_proxy": "Limpiar Geo Proxy",
"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",
"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}",
"proxy_url": "Proxy URL",
"proxy_placeholder": "http://proxy:port or socks5://proxy:port",
"geo_bypass": "Geo-bypass proxy (for geographic restrictions)",
"geo_bypass_placeholder": "http://proxy:port for bypassing geo-blocks"
},
"download": {
"preparing": "Preparando descarga...",
"starting": "🚀 Iniciando descarga...",
"fetching_info": "🔍 Obteniendo información del video...",
"preparing_streams": "🎯 Preparando flujos de video...",
"downloading_audio": "⏬ Descargando audio...",
"downloading_video": "⏬ Descargando video...",
"downloading_subtitle": "⏬ Descargando subtítulos...",
"downloading": "⏬ Descargando...",
"completed": "✅ ¡Descarga completada!",
"video_completed": "✅ ¡Descarga de video completada!",
"audio_completed": "✅ ¡Descarga de audio completada!",
"subtitle_completed": "✅ ¡Descarga de subtítulos completada!",
"completed_cleaning": "✅ ¡Descarga completada! Limpiando...",
"cancelled": "Descarga cancelada",
"processing_playlist": "📋 Procesando datos de lista de reproducción...",
"paused": "Descarga pausada",
"resumed": "Descarga reanudada",
"merging_formats": "✨ Post-procesamiento: Fusionando formatos...",
"removing_sponsor_segments": "✨ Post-procesamiento: Eliminando segmentos patrocinados...",
"speed": "Velocidad",
"eta": "Tiempo restante",
"please_enter_url": "Por favor ingresa una URL primero.",
"please_set_path": "Por favor establece una ruta de descarga primero.",
"please_enter_url_and_path": "Por favor ingresa URL y establece ruta de descarga.",
"please_select_format": "Por favor selecciona un formato primero.",
"downloading_fallback": "⚡ Descargando..."
},
"formats": {
"show_formats": "Mostrar formatos:",
"select": "Seleccionar",
"quality": "Calidad",
"extension": "Extensión",
"resolution": "Resolución",
"file_size": "Tamaño",
"codec": "Códec",
"audio": "Audio",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "Se combinará audio",
"has_audio": "✓ Tiene Audio",
"audio_only": "Solo Audio",
"best_4k": "Óptima (4K)",
"best_2k": "Óptima (2K)",
"high_1080p": "Alta (1080p)",
"high_720p": "Alta (720p)",
"medium_480p": "Media (480p)",
"low_quality": "Baja Calidad",
"best_audio": "Mejor Audio",
"high_audio": "Audio Alto",
"medium_audio": "Audio Medio",
"low_audio": "Audio Bajo",
"audio_only_resolution": "Solo audio",
"video_format": "Video Format:",
"audio_format": "Audio Format:",
"no_formats": "No formats available",
"loading": "Loading formats..."
},
"about": {
"title": "Acerca de YTSage",
"version": "Versión {version}",
"description": "Descargador moderno de YouTube con una interfaz limpia de PySide6.",
"author": "Por: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Detectado",
"missing": "✗ Faltante",
"not_available": "No Disponible"
},
"time_range": {
"title": "Descargar Sección del Video",
"help_text": "Descarga solo partes específicas de un video especificando rangos de tiempo.\nUsa formato HH:MM:SS o segundos. Deja inicio o fin vacío para descargar desde el principio o hasta el final.",
"time_range_group": "Rango de Tiempo",
"start_time": "Tiempo de Inicio:",
"end_time": "Tiempo de Fin:",
"start_time_placeholder": "00:00:00 (o dejar vacío para inicio)",
"end_time_placeholder": "00:10:00 (o dejar vacío para fin)",
"force_keyframes": "Forzar fotogramas clave en cortes (mejor precisión, más lento)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"invalid_format": "Invalid time format. Use HH:MM:SS format.",
"start_after_end": "Start time cannot be after end time."
},
"update": {
"title": "Actualizar yt-dlp",
"checking": "Verificando actualizaciones...",
"update_available": "¡Actualización disponible!\nVersión actual: {current}\nÚltima versión: {latest}",
"up_to_date": "yt-dlp está actualizado (versión {version})",
"already_up_to_date": "✅ ¡yt-dlp ya está actualizado!",
"could_not_determine": "No se pudieron determinar las versiones.",
"error_comparing": "Error al comparar versiones: {error}",
"update_available_failed": "¡Actualización disponible! (Comparación fallida)\nActual: {current}\nÚltima: {latest}",
"updating": "Actualizando...",
"initializing": "🚀 Inicializando proceso de actualización...",
"checking_current": "🔍 Verificando instalación actual...",
"found_at": "📍 Se encontró yt-dlp en: {path}",
"error_getting_path": "❌ Error al obtener la ruta de yt-dlp: {error}",
"updating_binary": "📦 Actualizando binario de yt-dlp administrado por la app...",
"update_failed": "❌ Falló la actualización de yt-dlp. Inténtalo de nuevo o verifica tu conexión a internet.",
"binary_updated": "✅ ¡Binario actualizado exitosamente!",
"update_failed_stderr": "❌ Falló la actualización de yt-dlp: {error}",
"update_timeout": "❌ Tiempo agotado en la actualización de yt-dlp.",
"unexpected_error": "❌ Error inesperado durante la actualización: {error}",
"update_success": "✅ ¡yt-dlp se ha actualizado exitosamente!",
"already_latest": "yt-dlp está actualizado (versión {version})",
"network_error": "❌ Error de red durante la actualización: {error}",
"general_error": "❌ La actualización falló: {error}",
"update_in_progress_title": "Actualización en curso",
"update_in_progress_message": "yt-dlp se está actualizando actualmente. Por favor espere un momento."
},
"settings": {
"title": "Configuración de Descarga",
"download_path": "Ruta de Descarga",
"browse": "Examinar...",
"speed_limit": "Límite de Velocidad",
"speed_limit_placeholder": "Ninguno",
"generic_mode": "Modo genérico",
"enable_generic_mode": "Habilitar modo genérico (compatibilidad con sitios que no son YouTube)",
"generic_mode_help": "Permite descargar desde Dailymotion, CBC Gem y otros sitios compatibles con yt-dlp.",
"auto_update_ytdlp": "Auto-Actualizar yt-dlp",
"enable_auto_updates": "Habilitar actualizaciones automáticas de yt-dlp",
"update_frequency": "Frecuencia de actualización:",
"check_startup": "Verificar en cada inicio (mínimo 1 hora entre verificaciones)",
"check_daily": "Verificar diariamente",
"check_weekly": "Verificar semanalmente",
"check_updates_now": "Actualizar yt-dlp",
"update_check_title": "Verificación de Actualizaciones",
"could_not_determine_version": "No se pudo determinar la versión actual de yt-dlp.",
"update_available_dialog": "¡Actualización disponible!\n\nActual: {current}\nÚltima: {latest}\n\nHaz clic en Aceptar para continuar con la actualización.",
"up_to_date_dialog": "¡yt-dlp está actualizado!\n\nVersión actual: {version}",
"error_checking_updates": "Error al verificar actualizaciones: {error}",
"settings_saved_title": "Configuración Guardada",
"settings_saved_message": "¡La configuración de auto-actualización se ha guardado exitosamente!",
"error_title": "Error",
"failed_save_settings": "Falló al guardar la configuración de auto-actualización.",
"error_saving_settings": "Error al guardar la configuración de auto-actualización: {error}",
"ytdlp_channel": "Canal de Lanzamiento de yt-dlp",
"ytdlp_channel_stable": "Estable (Versiones probadas)",
"ytdlp_channel_nightly": "Nightly (Actualizaciones diarias, recomendado por yt-dlp)",
"ytdlp_channel_description": "Elige entre versiones estables y nightly. Las versiones nightly se actualizan diariamente con las últimas correcciones y características. Puedes cambiar de canal en cualquier momento.",
"ytdlp_switching_channel": "Cambiando al canal {channel}...",
"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_app_updates": "Buscar actualizaciones de YTSage al iniciar",
"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.",
"current_status": "Estado Actual",
"current_version_label": "Versión actual de yt-dlp: Verificando...",
"last_check_label": "Última verificación de actualización: Nunca",
"next_check_label": "Próxima verificación: Basado en configuración",
"manual_check_button": "🔍 Verificar Actualizaciones Ahora",
"update_frequency_group": "Frecuencia de Actualización",
"save_settings": "Guardar Configuración",
"settings_saved_successfully": "✅ ¡Configuración guardada exitosamente!",
"error_saving": "❌ Error al guardar configuración: {error}",
"output_format_settings": "Configuración de Formato de Salida",
"force_output_format": "Forzar formato de salida al combinar",
"preferred_format": "Formato preferido:",
"force_format_help": "Cuando está habilitado, los videos combinados se convertirán a su formato preferido. Deshabilitar para dejar que yt-dlp decida automáticamente.",
"format_mp4": "MP4 (Más compatible)",
"format_webm": "WebM (Moderno, abierto)",
"format_mkv": "MKV (Rico en funciones)",
"audio_format_settings": "Configuración de formato de audio",
"force_audio_format": "Forzar formato de audio para descargas solo de audio",
"audio_normalization": "Normalización de audio (EBU R128)",
"audio_normalization_help": "Cuando está activado, las pistas de audio se normalizarán. Nota: esto requiere recodificación, por lo que se debe forzar un formato de audio específico (como MP3 o M4A).",
"preferred_audio_format": "Formato de audio preferido:",
"force_audio_format_help": "Cuando está habilitado, las descargas solo de audio se convertirán a su formato preferido. Esto solo se aplica al descargar formatos de audio.",
"audio_format_best": "Mejor (Sin conversión)",
"audio_format_aac": "AAC (Bueno para edición)",
"audio_format_mp3": "MP3 (Universal)",
"audio_format_flac": "FLAC (Sin pérdida)",
"audio_format_wav": "WAV (Sin comprimir)",
"audio_format_opus": "Opus (Eficiente)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "General",
"tab_format": "Formato",
"tab_file": "Archivo",
"concurrent_fragments": "Conexiones simultáneas",
"concurrent_fragments_help": "Número de conexiones por descarga. Los valores más altos evitan la limitación, pero pueden causar bloqueos temporales si se establecen demasiado altos. Predeterminado: 1.",
"defaults_settings": "Configuración de selección predeterminada",
"default_video_quality": "Resolución de video predeterminada (altura):",
"default_subtitle_language": "Idioma(s) de subtítulos predeterminado(s):",
"defaults_help": "Establece tu altura de video preferida y los idiomas de los subtítulos (separados por comas). Se seleccionarán automáticamente si están disponibles."
},
"main_ui": {
"url_placeholder": "Ingresa URL de video o lista de YouTube",
"url_placeholder_generic": "Ingresa la URL de un video o una lista de cualquier sitio compatible",
"merge_subtitles": "Combinar Subtítulos",
"save_thumbnail": "Guardar Miniatura",
"save_description": "Guardar Descripción",
"embed_chapters": "Incrustar Capítulos",
"subtitles_selected": "{count} seleccionados",
"all_selected": "Todos seleccionados",
"select_videos_all": "Seleccionar Videos... (Todos seleccionados)",
"please_enter_url": "Por favor ingresa primero una URL",
"error_title": "Error",
"cookie_file_selected_title": "Archivo de Cookies Seleccionado",
"cookie_file_selected_message": "Archivo de cookies seleccionado: {path}",
"browser_cookies_selected_title": "Cookies de Navegador Seleccionadas",
"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": "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": "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",
"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",
"sponsor_desc": "Promoción pagada, referencias pagadas y anuncios directos",
"selfpromo": "Autopromoción No Pagada",
"selfpromo_desc": "Promoción no pagada del propio contenido del creador",
"interaction": "Recordatorio de Interacción",
"interaction_desc": "Pedir a los espectadores que den like, se suscriban o sigan en redes sociales",
"intro": "Introducción",
"intro_desc": "Introducción del video que se puede omitir",
"outro": "Outro/Tarjetas Finales",
"outro_desc": "Créditos o cuando termina el video",
"preview": "Vista Previa/Resumen",
"preview_desc": "Resumen rápido de videos anteriores o vista previa de lo que viene",
"music_offtopic": "Sección No Musical",
"music_offtopic_desc": "Solo para videos musicales. Marca secciones no musicales",
"filler": "Relleno/Tangente",
"filler_desc": "Escenas tangenciales agregadas solo como relleno o humor"
},
"video_info": {
"channel": "Canal",
"views": "👁 Visualizaciones",
"likes": "👍 Me gusta",
"upload_date": "📅 Fecha de subida",
"duration": "⏱ Duración",
"unknown_channel": "Canal desconocido",
"unknown_date": "Fecha desconocida",
"unknown_title": "Título desconocido"
},
"command": {
"running": "Ejecutando...",
"run_command": "Ejecutar Comando"
},
"selection": {
"none_selected": "0 seleccionados",
"one_selected": "1 categoría seleccionada",
"count_selected": "{count} seleccionados"
},
"status": {
"ready": "Listo",
"file_exists": "⚠️ El archivo ya existe",
"video_file_exists": "⚠️ El archivo de video ya existe",
"audio_file_exists": "⚠️ El archivo de audio ya existe",
"subtitle_file_exists": "⚠️ El archivo de subtítulos ya existe",
"cancelling": "Cancelando descarga...",
"thumbnail_saved": "✅ Miniatura guardada: {filename}",
"thumbnail_error": "❌ Error de miniatura: {error}",
"thumbnail_no_image": "No hay miniatura disponible para guardar"
},
"errors": {
"playlist_no_videos": "Error: La lista de reproducción no contiene videos válidos.",
"playlist_no_url": "Error: No se pudo obtener la URL del primer video de la lista.",
"ytdlp_not_found": "Error: Ejecutable de yt-dlp no encontrado. Por favor instala yt-dlp primero.",
"ytdlp_not_found_path": "Error: Ejecutable de yt-dlp no encontrado. Esto podría deberse a una instalación incorrecta o un problema de PATH.",
"no_data_returned": "Error: yt-dlp no devolvió datos",
"no_format_info": "Error: No hay información de formato disponible.",
"analysis_timeout": "Error: Se agotó el tiempo de análisis. Por favor inténtalo de nuevo.",
"invalid_speed_limit": "❌ Error: Valor de límite de velocidad inválido en configuración.",
"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}",
"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}",
"private_video": "Este video podría ser privado. Por favor, utiliza cookies desde las opciones personalizadas."
},
"update_dialog": {
"title": "Actualización disponible",
"new_version_available": "¡Una nueva versión de YTSage está disponible!",
"current_version_label": "Versión actual:",
"latest_version_label": "Última versión:",
"changelog": "Registro de cambios",
"download_update": "Descargar Actualización",
"remind_later": "Recordar Más Tarde"
},
"playlist": {
"unknown": "Lista de reproducción desconocida",
"total_videos": "Total de Videos: {count}",
"display_format": "Lista de reproducción: {title} | {count} videos",
"select_videos_title": "Seleccionar Videos de la Lista de Reproducción",
"save_as": "Guardar playlist como",
"save_success_title": "Éxito",
"saved_successfully": "Playlist guardada correctamente.",
"save_error_title": "Error al guardar",
"no_videos_to_save": "¡No se han recopilado entradas de la lista de reproducción!",
"save_error_msg": "Error al guardar la lista de reproducción."
},
"subtitle_selection": {
"count_selected": "{count} seleccionados"
},
"file_exists_dialog": {
"title": "El archivo ya existe",
"message": "El archivo ya existe:\n{filename}",
"info": "Este video ya ha sido descargado."
},
"auto_update": {
"last_check_never": "Última verificación: Nunca",
"last_check": "Última verificación: {time}",
"next_check_disabled": "Próxima verificación: Deshabilitada",
"next_check_startup": "Próxima verificación: Al iniciar",
"next_check_overdue": "Próxima verificación: Ahora (atrasada)",
"next_check": "Próxima verificación: {time}",
"next_check_error": "Próxima verificación: Error al calcular",
"checking": "🔄 Verificando...",
"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",
"invalid_format": "Formato de URL no válido",
"invalid_scheme": "La URL debe comenzar con http:// o https://",
"missing_domain": "URL no válida: falta el nombre de dominio",
"unsupported_platform": "YTSage solo admite URLs de YouTube y YouTube Music.\nEl dominio '{domain}' no es compatible.",
"invalid_youtu_be": "URL de youtu.be no válida: falta el ID del video"
},
"ytdlp_errors": {
"private_video": "Este es un video privado. Puede descargarlo iniciando sesión en su cuenta usando cookies.\nVaya a 'Opciones Personalizadas' → 'Iniciar sesión con Cookies' → 'Extraer cookies del navegador' para autenticarse.",
"age_restricted": "Este video tiene restricción de edad. Debe iniciar sesión para acceder a él.\nUse 'Opciones Personalizadas' → 'Iniciar sesión con Cookies' para autenticarse con su cuenta.",
"geo_blocked": "Este video no está disponible en su región (bloqueado geográficamente).\nPuede necesitar usar una VPN o el video puede estar restringido en su país.",
"video_unavailable": "Este video ha sido eliminado o ya no está disponible.\nEl video puede haber sido eliminado por el usuario que lo subió o eliminado debido a violaciones de políticas.",
"live_stream": "Esta es una transmisión en vivo que no se puede descargar mientras está activa.\nEspere a que termine la transmisión y luego intente descargar la versión archivada.",
"playlist_error": "No se puede acceder a esta lista de reproducción. Puede ser privada, estar eliminada o vacía.\nVerifique si la lista de reproducción existe y es públicamente accesible.",
"network_error": "Error de conexión de red. Verifique su conexión a Internet e intente nuevamente.\nSi el problema persiste, el servidor de video puede no estar disponible temporalmente.",
"invalid_url": "URL no válida o no compatible. Verifique el enlace e intente nuevamente.\nAsegúrese de usar una URL válida de YouTube, Vimeo u otra plataforma compatible.",
"premium_content": "Este contenido requiere YouTube Premium o membresía del canal.\nDebe iniciar sesión con una cuenta que tenga acceso a este contenido.",
"copyright_blocked": "Este video está bloqueado debido a reclamaciones de derechos de autor.\nEl propietario del contenido ha restringido el acceso a este video.",
"extraction_failed": "Error al extraer información del video. Esto podría ser un problema temporal.\nIntente nuevamente en unos minutos o verifique si el enlace del video es correcto.",
"generic_error": "No se pudo extraer la información del video. Verifique su enlace.\nDetalles técnicos: {error}"
},
"history": {
"title": "Historial de Descargas",
"clear_all": "Borrar Todo",
"clear_confirm_title": "¿Borrar Historial?",
"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",
"remove": "Eliminar",
"file_not_found": "Archivo no encontrado",
"file_not_found_message": "El archivo fue movido o eliminado:\n{path}",
"downloaded_on": "Descargado: {date}",
"file_size": "Tamaño: {size}",
"audio_download": "Audio",
"video_download": "Video",
"remove_confirm_title": "¿Eliminar?",
"remove_confirm_message": "¿Eliminar del historial?\n\n{title}",
"item_removed": "Eliminado",
"history_cleared": "Historial borrado",
"entries_count": "{count} descargas",
"one_entry": "1 descarga",
"redownload_confirm_title": "¿Descargar de Nuevo?",
"redownload_confirm_message": "¿Descargar nuevamente?\n\n{title}",
"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",
"current_version": "Versión Actual:",
"latest_version": "Última Versión:",
"status_up_to_date": "✓ Actualizado",
"status_update_available": "⚠ Actualización disponible",
"status_not_installed": "✗ No instalado",
"status_idle": "Haga clic en 'Comprobar Versión' para comenzar",
"status_checking": "🔄 Verificando versión...",
"check_updates": "Comprobar Versión",
"check_failed": "Error al verificar la versión. Por favor, verifique su conexión a internet.",
"description": "Compruebe su versión de FFmpeg y compárela con la última versión disponible.",
"guide_info": " Para instalar o actualizar FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>haga clic aquí para ver nuestra guía completa de instalación</a>."
},
"deno": {
"setup_required": "Configuración de Deno requerida",
"setup_description": "YTSage requiere Deno para ejecutar ciertas funciones.<br><br>No se encontró Deno en el directorio local de la aplicación. YTSage necesita configurar Deno para su sistema {os_name}.<br><br>Haga clic en 'Configurar Deno' para descargar e instalar automáticamente.",
"setup_button": "Configurar Deno",
"downloading": "Descargando Deno...",
"extracting": "Extrayendo Deno...",
"verifying": "Verificando Deno...",
"success": "¡Deno se instaló correctamente!",
"download_failed": "Descarga fallida",
"download_error": "Error al descargar Deno: {error}",
"verification_failed": "Error en la verificación SHA256. El archivo descargado puede estar dañado o alterado.",
"setup_failed": "Error al configurar Deno. Es posible que algunas funciones no funcionen correctamente.",
"setup_error": "Error de configuración"
},
"deno_updater": {
"title": "Verificador y Actualizador de Versión de Deno",
"description": "Verifique su versión de Deno y actualice a la última versión.",
"current_version": "Versión Actual:",
"latest_version": "Última Versión:",
"status_idle": "Haga clic en 'Buscar Actualizaciones' para comenzar",
"status_checking": "🔄 Buscando actualizaciones...",
"status_up_to_date": "✓ Actualizado",
"status_update_available": "⚠ Actualización disponible",
"status_not_installed": "✗ No instalado",
"check_updates": "Buscar Actualizaciones",
"update_now": "Actualizar Deno",
"updating": "🔄 Actualizando Deno...",
"update_success": "✅ ¡Deno se ha actualizado correctamente!",
"update_failed": "❌ Error en la actualización: {error}",
"check_failed": "Error al buscar actualizaciones. Por favor, verifique su conexión a Internet."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Français (French)",
"select_language": "Sélectionner la langue :",
"current_language": "Langue actuelle : {language}",
"restart_required": "Les modifications de langue prendront effet après le redémarrage de l'application.",
"english": "Anglais",
"spanish": "Espagnol",
"portuguese": "Portugais",
"russian": "Russe",
"chinese": "Chinois",
"german": "Allemand",
"french": "Français",
"hindi": "Hindi",
"indonesian": "Indonésien",
"turkish": "Turc",
"polish": "Polonais",
"italian": "Italien",
"arabic": "Arabe",
"japanese": "Japonais",
"help_text": "Sélectionnez votre langue préférée pour l'interface.",
"restart_notice": "Le changement de langue prendra effet après le redémarrage de l'application."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Prêt"
},
"formats": {
"show_formats": "Afficher les formats :",
"video_format": "Format vidéo :",
"audio_format": "Format audio :",
"no_formats": "Aucun format disponible",
"loading": "Chargement des formats...",
"select": "Sélectionner",
"quality": "Qualité",
"extension": "Extension",
"resolution": "Résolution",
"file_size": "Taille du fichier",
"codec": "Codec",
"audio": "Audio",
"fps": "IPS",
"hdr": "HDR",
"will_merge_audio": "L'audio sera fusionné",
"has_audio": "✓ Contient de l'audio",
"audio_only": "Audio uniquement",
"best_4k": "Meilleure (4K)",
"best_2k": "Meilleure (2K)",
"high_1080p": "Haute (1080p)",
"high_720p": "Haute (720p)",
"medium_480p": "Moyenne (480p)",
"low_quality": "Qualité faible",
"best_audio": "Meilleur audio",
"high_audio": "Audio élevé",
"medium_audio": "Audio moyen",
"low_audio": "Audio faible",
"audio_only_resolution": "Audio uniquement"
},
"buttons": {
"reset": "Réinitialiser",
"download": "Télécharger",
"pause": "Pause",
"resume": "Reprendre",
"cancel": "Annuler",
"browse": "Parcourir",
"clear": "Effacer",
"ok": "OK",
"apply": "Appliquer",
"close": "Fermer",
"run_command": "Exécuter la commande",
"custom_command_help": "Aide",
"about": "À propos",
"analyze": "Analyser",
"paste_url": "Coller l'URL",
"select_videos": "Sélectionner les vidéos...",
"video": "Vidéo",
"audio_only": "Audio uniquement",
"custom_options": "Options personnalisées",
"trim_video": "Découper la vidéo",
"download_settings": "Paramètres de téléchargement",
"update": "Mettre à jour yt-dlp",
"select_defaults": "Sélectionner par défaut",
"select_all": "Tout sélectionner",
"deselect_all": "Tout désélectionner",
"open_folder": "Ouvrir l'emplacement du dossier",
"history": "Historique",
"save_playlist": "Enregistrer la playlist sous"
},
"dialogs": {
"custom_options": "Options personnalisées",
"settings": "Paramètres",
"select_folder": "Sélectionner le dossier de téléchargement",
"sponsorblock_categories": "Catégories SponsorBlock",
"sponsorblock_description": "Choisissez les types de segments vidéo à supprimer automatiquement pendant le téléchargement.\nSponsorBlock utilise des données soumises par la communauté pour identifier ces segments.",
"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",
"ytdlp_log_title": "Journal yt-dlp",
"filter_playlist_placeholder": "Filtrer les vidéos..."
},
"tabs": {
"cookies": "Se connecter avec des cookies",
"custom_command": "Commande personnalisée",
"proxy": "Proxy",
"language": "Langue",
"updater": "Mise à jour"
},
"cookies": {
"help_text": "Choisissez comment fournir les cookies pour l'authentification.\nCeci permet de télécharger des vidéos privées et des fichiers audio de haute qualité.",
"cookie_source": "Source des cookies",
"use_cookie_file": "Utiliser un fichier de cookies",
"extract_from_browser": "Extraire du navigateur",
"recommended": "Recommandé",
"cookie_file": "Fichier de cookies",
"cookie_file_placeholder": "Chemin vers le fichier cookies.txt...",
"browser_selection": "Sélection du navigateur",
"browser_help": "Sélectionnez le navigateur depuis lequel extraire les cookies :",
"browser_label": "Navigateur :",
"profile_label": "Profil :",
"profile_placeholder": "Par défaut",
"browser_extract_message": "Les cookies du navigateur seront extraits lors de l'application",
"file_selected_message": "Fichier de cookies sélectionné - Cliquez sur OK pour appliquer",
"select_file_title": "Sélectionner le fichier de cookies",
"file_filter": "Fichiers de cookies (*.txt *.lwp)",
"file_selected_title": "Fichier de cookies appliqué",
"file_applied_message": "Fichier de cookies appliqué : {path}",
"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 des cookies ont été effacés",
"active_browser": "✓ Actifs : cookies du navigateur ({browser})",
"active_file": "✓ Actif : fichier cookies ({file})",
"none_active": "○ Aucun cookie actif",
"remember_settings": "Mémoriser les paramètres des cookies au prochain démarrage"
},
"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.",
"input_label": "Arguments yt-dlp :",
"input_placeholder": "Entrez les arguments yt-dlp ici...\n\nEx : --extract-audio --audio-format mp3",
"output_label": "Sortie de la commande :",
"output_placeholder": "La sortie de la commande apparaîtra ici...",
"command_placeholder": "Entrer une commande yt-dlp personnalisée...",
"command_help": "Espaces réservés disponibles :\n{url} - URL de la vidéo\n{output} - Dossier de sortie\n\nExemple : --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Proxy principal",
"main_proxy_help": "Serveur proxy principal pour tous les téléchargements. Prend en charge les protocoles HTTP/HTTPS et SOCKS5.",
"proxy_url_label": "URL du proxy :",
"proxy_url_placeholder": "http://proxy:port ou socks5://proxy:port",
"proxy_examples": "Exemples :\n• HTTP : http://proxy.example.com:8080\n• SOCKS5 : socks5://proxy.example.com:1080",
"geo_proxy": "Proxy de contournement géographique",
"geo_proxy_help": "Proxy secondaire spécialement pour contourner les restrictions géographiques.",
"geo_proxy_url_label": "URL du proxy de contournement géographique :",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Effacer le proxy principal",
"clear_geo_proxy": "Effacer le proxy géographique",
"proxy_url": "URL du proxy",
"proxy_placeholder": "http://proxy:port ou socks5://proxy:port",
"geo_bypass": "Proxy de contournement géographique (pour les restrictions géographiques)",
"geo_bypass_placeholder": "http://proxy:port pour contourner le géo-blocage",
"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é",
"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...",
"starting": "🚀 Démarrage du téléchargement...",
"fetching_info": "🔍 Récupération des informations vidéo...",
"preparing_streams": "🎯 Préparation des flux vidéo...",
"downloading_audio": "⏬ Téléchargement audio...",
"downloading_video": "⏬ Téléchargement vidéo...",
"downloading_subtitle": "⏬ Téléchargement des sous-titres...",
"downloading": "⏬ Téléchargement...",
"completed": "✅ Téléchargement terminé !",
"video_completed": "✅ Téléchargement vidéo terminé !",
"audio_completed": "✅ Téléchargement audio terminé !",
"subtitle_completed": "✅ Téléchargement des sous-titres terminé !",
"completed_cleaning": "✅ Téléchargement terminé ! Nettoyage...",
"cancelled": "Téléchargement annulé",
"processing_playlist": "📋 Traitement des données de la playlist...",
"paused": "Téléchargement en pause",
"resumed": "Téléchargement repris",
"merging_formats": "✨ Post-traitement : Fusion des formats...",
"removing_sponsor_segments": "✨ Post-traitement : Suppression des segments sponsorisés...",
"speed": "Vitesse",
"eta": "Temps restant",
"please_enter_url": "Veuillez d'abord entrer une URL.",
"please_set_path": "Veuillez d'abord définir un chemin de téléchargement.",
"please_enter_url_and_path": "Veuillez entrer une URL et définir un chemin de téléchargement.",
"please_select_format": "Veuillez d'abord sélectionner un format.",
"downloading_fallback": "⚡ Téléchargement..."
},
"update": {
"title": "Mettre à jour yt-dlp",
"checking": "Vérification des mises à jour...",
"update_available": "Mise à jour disponible !\nVersion actuelle : {current}\nDernière version : {latest}",
"up_to_date": "yt-dlp est à jour (Version {version})",
"could_not_determine": "Impossible de déterminer les versions.",
"error_comparing": "Erreur lors de la comparaison des versions : {error}",
"update_available_failed": "Mise à jour disponible ! (Comparaison échouée)\nActuelle : {current}\nDernière : {latest}",
"updating": "Mise à jour...",
"initializing": "🚀 Initialisation du processus de mise à jour...",
"checking_current": "🔍 Vérification de l'installation actuelle...",
"found_at": "📍 yt-dlp trouvé à : {path}",
"error_getting_path": "❌ Erreur lors de la récupération du chemin yt-dlp : {error}",
"updating_binary": "📦 Mise à jour du binaire yt-dlp géré par l'application...",
"update_failed": "❌ Échec de la mise à jour yt-dlp. Veuillez réessayer ou vérifier votre connexion Internet.",
"binary_updated": "✅ Binaire mis à jour avec succès !",
"update_failed_stderr": "❌ Échec de la mise à jour yt-dlp : {error}",
"update_timeout": "❌ Délai d'attente de la mise à jour yt-dlp dépassé.",
"unexpected_error": "❌ Erreur inattendue lors de la mise à jour : {error}",
"already_up_to_date": "✅ yt-dlp est déjà à jour !",
"update_success": "✅ yt-dlp a été mis à jour avec succès !",
"already_latest": "yt-dlp est à jour (version {version})",
"network_error": "❌ Erreur réseau pendant la mise à jour : {error}",
"general_error": "❌ La mise à jour a échoué : {error}",
"update_in_progress_title": "Mise à jour en cours",
"update_in_progress_message": "yt-dlp est en cours de mise à jour. Veuillez patienter un instant."
},
"about": {
"title": "À propos de YTSage",
"version": "Version {version}",
"description": "Téléchargeur YouTube moderne avec une interface PySide6 propre.",
"author": "Par : {author}",
"github": "GitHub : {repo}",
"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.",
"detected": "✓ Détecté",
"missing": "✗ Manquant",
"not_available": "Non disponible"
},
"time_range": {
"title": "Découper la vidéo",
"time_range_group": "Plage horaire",
"start_time": "Heure de début (HH:MM:SS)",
"end_time": "Heure de fin (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Heure de début (HH:MM:SS)",
"end_time_placeholder": "Heure de fin (HH:MM:SS)",
"force_keyframes": "Forcer les images clés aux points de coupe",
"help_text": "Définissez les heures de début et de fin pour découper la vidéo.\nLaissez vide pour télécharger la vidéo complète.",
"invalid_format": "Format d'heure invalide. Utilisez le format HH:MM:SS.",
"start_after_end": "L'heure de début ne peut pas être après l'heure de fin."
},
"settings": {
"title": "Paramètres de téléchargement",
"download_path": "Chemin de téléchargement",
"browse": "Parcourir...",
"speed_limit": "Limite de vitesse",
"speed_limit_placeholder": "Aucune",
"generic_mode": "Mode générique",
"enable_generic_mode": "Activer le mode générique (prise en charge des sites non-YouTube)",
"generic_mode_help": "Permet le téléchargement depuis Dailymotion, CBC Gem et d'autres sites pris en charge par yt-dlp.",
"auto_update_ytdlp": "Mise à jour automatique de yt-dlp",
"enable_auto_updates": "Activer les mises à jour automatiques de yt-dlp",
"update_frequency": "Fréquence de mise à jour :",
"check_startup": "Vérifier à chaque démarrage (minimum 1 heure entre les vérifications)",
"check_daily": "Vérifier quotidiennement",
"check_weekly": "Vérifier hebdomadairement",
"check_updates_now": "Mettre à jour yt-dlp",
"update_check_title": "Vérification de mise à jour",
"could_not_determine_version": "Impossible de déterminer la version actuelle de yt-dlp.",
"update_available_dialog": "Mise à jour disponible !\n\nActuelle : {current}\nDernière : {latest}\n\nCliquez sur OK pour continuer la mise à jour.",
"up_to_date_dialog": "yt-dlp est à jour !\n\nVersion actuelle : {version}",
"error_checking_updates": "Erreur lors de la vérification des mises à jour : {error}",
"settings_saved_title": "Paramètres sauvegardés",
"settings_saved_message": "Les paramètres de mise à jour automatique ont été sauvegardés avec succès !",
"error_title": "Erreur",
"failed_save_settings": "Échec de la sauvegarde des paramètres de mise à jour automatique.",
"error_saving_settings": "Erreur lors de la sauvegarde des paramètres de mise à jour automatique : {error}",
"ytdlp_channel": "Canal de version yt-dlp",
"ytdlp_channel_stable": "Stable (Versions testées)",
"ytdlp_channel_nightly": "Nightly (Mises à jour quotidiennes, recommandé par yt-dlp)",
"ytdlp_channel_description": "Choisissez entre les versions stables et nightly. Les versions nightly sont mises à jour quotidiennement avec les dernières corrections et fonctionnalités. Vous pouvez changer de canal à tout moment.",
"ytdlp_switching_channel": "Passage au canal {channel}...",
"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_app_updates": "Vérifier les mises à jour YTSage au démarrage",
"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.",
"current_status": "État actuel",
"current_version_label": "Version actuelle de yt-dlp : Vérification...",
"last_check_label": "Dernière vérification de mise à jour : Jamais",
"next_check_label": "Prochaine vérification : Basée sur les paramètres",
"manual_check_button": "🔍 Vérifier les mises à jour maintenant",
"update_frequency_group": "Fréquence de mise à jour",
"save_settings": "Sauvegarder les paramètres",
"settings_saved_successfully": "✅ Paramètres sauvegardés avec succès !",
"error_saving": "❌ Erreur lors de la sauvegarde des paramètres : {error}",
"output_format_settings": "Paramètres du format de sortie",
"force_output_format": "Forcer le format de sortie lors de la fusion",
"preferred_format": "Format préféré :",
"force_format_help": "Lorsqu'activé, les vidéos fusionnées seront converties vers votre format préféré. Désactivez pour laisser yt-dlp décider automatiquement.",
"format_mp4": "MP4 (Plus compatible)",
"format_webm": "WebM (Moderne, ouvert)",
"format_mkv": "MKV (Riche en fonctionnalités)",
"audio_format_settings": "Paramètres du format audio",
"force_audio_format": "Forcer le format audio pour les téléchargements audio uniquement",
"audio_normalization": "Normalisation audio (EBU R128)",
"audio_normalization_help": "Lorsqu'il est activé, les pistes audio seront normalisées. Remarque : Cela nécessite un réencodage, un format audio spécifique (comme MP3 ou M4A) doit donc être forcé.",
"preferred_audio_format": "Format audio préféré :",
"force_audio_format_help": "Lorsqu'activé, les téléchargements audio uniquement seront convertis dans votre format préféré. Cela s'applique uniquement lors du téléchargement de formats audio.",
"audio_format_best": "Meilleur (Pas de conversion)",
"audio_format_aac": "AAC (Bon pour l'édition)",
"audio_format_mp3": "MP3 (Universel)",
"audio_format_flac": "FLAC (Sans perte)",
"audio_format_wav": "WAV (Non compressé)",
"audio_format_opus": "Opus (Efficace)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "Général",
"tab_format": "Format",
"tab_file": "Fichier",
"concurrent_fragments": "Connexions simultanées",
"concurrent_fragments_help": "Nombre de connexions par téléchargement. Des valeurs plus élevées contournent le bridage mais peuvent entraîner des blocages temporaires si elles sont trop élevées. Par défaut : 1.",
"defaults_settings": "Paramètres de sélection par défaut",
"default_video_quality": "Résolution vidéo par défaut (hauteur) :",
"default_subtitle_language": "Langue(s) des sous-titres par défaut :",
"defaults_help": "Définissez votre hauteur de vidéo préférée et les langues des sous-titres (séparées par des virgules). Elles seront sélectionnées automatiquement si elles sont disponibles."
},
"main_ui": {
"url_placeholder": "Entrer l'URL de la vidéo ou de la playlist YouTube",
"url_placeholder_generic": "Entrez l'URL d'une vidéo ou d'une playlist depuis n'importe quel site pris en charge",
"merge_subtitles": "Fusionner les sous-titres",
"save_thumbnail": "Sauvegarder la miniature",
"save_description": "Sauvegarder la description",
"embed_chapters": "Intégrer les chapitres",
"subtitles_selected": "{count} sélectionné(s)",
"all_selected": "Tous sélectionnés",
"select_videos_all": "Sélectionner les vidéos... (Tous sélectionnés)",
"please_enter_url": "Veuillez d'abord entrer une URL",
"error_title": "Erreur",
"cookie_file_selected_title": "Fichier de cookies sélectionné",
"cookie_file_selected_message": "Fichier de cookies sélectionné : {path}",
"browser_cookies_selected_title": "Cookies de navigateur sélectionnés",
"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": "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": "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",
"settings_tooltip": "Chemin actuel : {path}\nLimite de vitesse : {speed_limit}",
"speed_limit_none": "Aucune",
"open_folder_error": "Impossible douvrir le dossier : {error}",
"time_range_set": "Section définie : {section}"
},
"sponsorblock": {
"sponsor": "Sponsor",
"sponsor_desc": "Publicité payante, recommandations payantes et publicité directe",
"selfpromo": "Auto-promotion non payante",
"selfpromo_desc": "Publicité non payante pour le contenu propre du créateur",
"interaction": "Rappel d'interaction",
"interaction_desc": "Les spectateurs sont invités à aimer, s'abonner ou suivre sur les réseaux sociaux",
"intro": "Intro",
"intro_desc": "Introduction de la vidéo qui peut être ignorée",
"outro": "Outro/Cartes de fin",
"outro_desc": "Crédits ou fin de la vidéo",
"preview": "Aperçu/Récapitulatif",
"preview_desc": "Bref récapitulatif des vidéos précédentes ou aperçu du contenu à venir",
"music_offtopic": "Section hors-musique",
"music_offtopic_desc": "Uniquement pour les vidéos musicales. Marque les sections non-musicales",
"filler": "Digression de remplissage",
"filler_desc": "Scènes de diversion ajoutées uniquement comme remplissage ou pour l'humour"
},
"video_info": {
"channel": "Chaîne",
"views": "👁 Vues",
"likes": "👍 J'aime",
"upload_date": "📅 Date de mise en ligne",
"duration": "⏱ Durée",
"unknown_channel": "Chaîne inconnue",
"unknown_date": "Date inconnue",
"unknown_title": "Titre inconnu"
},
"command": {
"running": "En cours...",
"run_command": "Exécuter la commande"
},
"selection": {
"none_selected": "0 sélectionné",
"one_selected": "1 catégorie sélectionnée",
"count_selected": "{count} sélectionné(s)"
},
"status": {
"ready": "Prêt",
"file_exists": "⚠️ Le fichier existe déjà",
"video_file_exists": "⚠️ Le fichier vidéo existe déjà",
"audio_file_exists": "⚠️ Le fichier audio existe déjà",
"subtitle_file_exists": "⚠️ Le fichier de sous-titres existe déjà",
"cancelling": "Annulation du téléchargement...",
"thumbnail_saved": "✅ Miniature enregistrée : {filename}",
"thumbnail_error": "❌ Erreur de miniature : {error}",
"thumbnail_no_image": "Aucune miniature disponible à enregistrer"
},
"errors": {
"playlist_no_videos": "Erreur : La playlist ne contient aucune vidéo valide.",
"playlist_no_url": "Erreur : Impossible d'obtenir l'URL de la première vidéo de la playlist.",
"ytdlp_not_found": "Erreur : Exécutable yt-dlp introuvable. Veuillez d'abord installer yt-dlp.",
"ytdlp_not_found_path": "Erreur : Exécutable yt-dlp introuvable. Cela pourrait être dû à une installation incorrecte ou à un problème de PATH.",
"no_data_returned": "Erreur : Aucune donnée renvoyée par yt-dlp",
"no_format_info": "Erreur : Aucune information de format disponible.",
"analysis_timeout": "Erreur : Délai d'analyse dépassé. Veuillez réessayer.",
"invalid_speed_limit": "❌ Erreur : Valeur de limite de vitesse invalide définie dans les paramètres.",
"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}",
"download_failed_return_code_conflict": "Téléchargement échoué avec le code de retour {return_code}. Cela peut être dû à un conflit entre plusieurs installations de yt-dlp. Essayez de désinstaller toute version installée au niveau du système (par ex. via snap ou apt) puis redémarrez lapplication.",
"download_failed_return_code": "Téléchargement échoué avec le code de retour {return_code}",
"direct_command_error": "Erreur dans la commande directe : {error}",
"private_video": "Cette vidéo pourrait être privée. Veuillez utiliser des cookies depuis les options personnalisées."
},
"update_dialog": {
"title": "Mise à jour disponible",
"new_version_available": "Une nouvelle version de YTSage est disponible !",
"current_version_label": "Version actuelle :",
"latest_version_label": "Dernière version :",
"changelog": "Journal des modifications",
"download_update": "Télécharger la mise à jour",
"remind_later": "Me le rappeler plus tard"
},
"playlist": {
"unknown": "Playlist inconnue",
"total_videos": "Total des vidéos : {count}",
"display_format": "Playlist : {title} | {count} vidéos",
"select_videos_title": "Sélectionner les Vidéos de la Playlist",
"save_as": "Enregistrer la playlist sous",
"save_success_title": "Succès",
"saved_successfully": "Playlist enregistrée avec succès.",
"save_error_title": "Erreur d'enregistrement",
"no_videos_to_save": "Aucune vidéo de la playlist n'a été récupérée !",
"save_error_msg": "Échec de l'enregistrement de la playlist."
},
"subtitle_selection": {
"count_selected": "{count} sélectionné(s)"
},
"file_exists_dialog": {
"title": "Le fichier existe déjà",
"message": "Le fichier existe déjà :\n{filename}",
"info": "Cette vidéo a déjà été téléchargée."
},
"auto_update": {
"last_check_never": "Dernière vérification : Jamais",
"last_check": "Dernière vérification : {time}",
"next_check_disabled": "Prochaine vérification : Désactivée",
"next_check_startup": "Prochaine vérification : Au démarrage",
"next_check_overdue": "Prochaine vérification : Maintenant (en retard)",
"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",
"current_version": "Version actuelle de yt-dlp : {version}"
},
"url_validation": {
"empty_url": "L'URL ne peut pas être vide",
"invalid_format": "Format d'URL invalide",
"invalid_scheme": "L'URL doit commencer par http:// ou https://",
"missing_domain": "URL invalide : nom de domaine manquant",
"unsupported_platform": "YTSage ne prend en charge que les URLs YouTube et YouTube Music.\nLe domaine '{domain}' n'est pas pris en charge.",
"invalid_youtu_be": "URL youtu.be invalide : ID de vidéo manquant"
},
"ytdlp_errors": {
"private_video": "Ceci est une vidéo privée. Vous pouvez la télécharger en vous connectant à votre compte avec des cookies.\nAllez dans 'Options Personnalisées' → 'Se connecter avec des Cookies' → 'Extraire les cookies du navigateur' pour vous authentifier.",
"age_restricted": "Cette vidéo est soumise à une restriction d'âge. Vous devez être connecté pour y accéder.\nUtilisez 'Options Personnalisées' → 'Se connecter avec des Cookies' pour vous authentifier avec votre compte.",
"geo_blocked": "Cette vidéo n'est pas disponible dans votre région (géo-bloquée).\nVous pourriez avoir besoin d'utiliser un VPN ou la vidéo pourrait être restreinte dans votre pays.",
"video_unavailable": "Cette vidéo a été supprimée ou n'est plus disponible.\nLa vidéo a peut-être été supprimée par l'uploader ou retirée en raison de violations de politique.",
"live_stream": "Ceci est un flux en direct qui ne peut pas être téléchargé pendant qu'il est actif.\nAttendez la fin du flux, puis essayez de télécharger la version archivée.",
"playlist_error": "Impossible d'accéder à cette playlist. Elle peut être privée, supprimée ou vide.\nVérifiez si la playlist existe et est accessible publiquement.",
"network_error": "Erreur de connexion réseau. Veuillez vérifier votre connexion Internet et réessayer.\nSi le problème persiste, le serveur vidéo pourrait être temporairement indisponible.",
"invalid_url": "URL invalide ou non prise en charge. Veuillez vérifier le lien et réessayer.\nAssurez-vous d'utiliser une URL YouTube, Vimeo ou autre plateforme prise en charge valide.",
"premium_content": "Ce contenu nécessite YouTube Premium ou l'adhésion à la chaîne.\nVous devez être connecté avec un compte ayant accès à ce contenu.",
"copyright_blocked": "Cette vidéo est bloquée en raison de réclamations pour droits d'auteur.\nLe propriétaire du contenu a restreint l'accès à cette vidéo.",
"extraction_failed": "Échec de l'extraction des informations vidéo. Cela pourrait être un problème temporaire.\nVeuillez réessayer dans quelques minutes ou vérifier si le lien vidéo est correct.",
"generic_error": "Impossible d'extraire les informations vidéo. Veuillez vérifier votre lien.\nDétails techniques : {error}"
},
"history": {
"title": "Historique des Téléchargements",
"clear_all": "Tout Effacer",
"clear_confirm_title": "Effacer l'Historique?",
"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 lhistorique...",
"search_placeholder": "Rechercher...",
"open_location": "Ouvrir l'Emplacement",
"redownload": "Télécharger à Nouveau",
"remove": "Supprimer",
"file_not_found": "Fichier non trouvé",
"file_not_found_message": "Le fichier a été déplacé ou supprimé:\n{path}",
"downloaded_on": "Téléchargé: {date}",
"file_size": "Taille: {size}",
"audio_download": "Audio",
"video_download": "Vidéo",
"remove_confirm_title": "Supprimer?",
"remove_confirm_message": "Supprimer de l'historique?\n\n{title}",
"item_removed": "Supprimé",
"history_cleared": "Historique effacé",
"entries_count": "{count} téléchargements",
"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é",
"no_url_error": "Aucune URL trouvée dans lentrée dhistorique",
"redownload_failed": "Impossible de relancer le téléchargement : {error}"
},
"ffmpeg": {
"installation_title": "Installation de FFmpeg",
"installation_message": "YTSage a besoin de FFmpeg pour traiter les vidéos.\n\nChoisissez une option dinstallation cidessous :",
"install_button": "Installer FFmpeg",
"manual_guide": "Guide manuel",
"installation_failed": "Linstallation de FFmpeg a rencontré un problème.",
"already_installed": "FFmpeg est déjà installé !",
"installation_complete": "Installation terminée. Vous pouvez fermer cette boîte de dialogue et continuer à utiliser YTSage.",
"installing": "Installation de FFmpeg... Veuillez patienter",
"install_success": "FFmpeg a été installé avec succès !",
"installation_complete_close": "Installation terminée. Vous pouvez maintenant fermer cette boîte de dialogue et continuer à utiliser YTSage.",
"try_manual": "Veuillez essayer dutiliser le guide dinstallation manuel à la place."
},
"ytdlp_setup": {
"required_title": "Configuration de yt-dlp requise",
"description": "YTSage nécessite yt-dlp pour télécharger des vidéos.<br><br>yt-dlp est introuvable dans le répertoire local de lapplication. YTSage doit configurer yt-dlp pour votre système {os_name}.<br><br>Veuillez choisir une option cidessous :",
"option_auto": "Télécharger automatiquement (recommandé)",
"option_manual": "Sélectionner le chemin manuellement",
"setup_button": "Configurer yt-dlp",
"downloading": "Téléchargement de yt-dlp...",
"success": "yt-dlp a été installé avec succès !",
"error": "Erreur : {error}",
"download_failed_title": "Téléchargement échoué",
"download_failed_message": "Échec du téléchargement de yt-dlp : {error}",
"select_executable_title": "Sélectionner lexécutable yt-dlp",
"copied_to": "yt-dlp copié avec succès vers {path}",
"setup_error_title": "Erreur de configuration",
"copy_error": "Erreur lors de la copie de yt-dlp dans le répertoire de lapplication : {error}",
"invalid_executable_title": "Exécutable invalide",
"invalid_executable_message": "Le fichier sélectionné ne semble pas être un exécutable yt-dlp valide.",
"verify_error": "Erreur lors de la vérification de lexécutable yt-dlp : {error}",
"setup_failed_title": "Configuration échouée",
"setup_failed_message": "La configuration de yt-dlp a échoué. Certaines fonctionnalités peuvent ne pas fonctionner correctement.",
"success_dialog_title": "Configuration de yt-dlp",
"success_dialog_message": "yt-dlp a été configuré avec succès à lemplacement :\n{path}",
"file_filter_windows": "Fichiers exécutables (*.exe)",
"file_filter_all": "Tous les fichiers (*)"
},
"ffmpeg_updater": {
"title": "Vérificateur de Version FFmpeg",
"current_version": "Version Actuelle:",
"latest_version": "Dernière Version:",
"status_up_to_date": "✓ À jour",
"status_update_available": "⚠ Mise à jour disponible",
"status_not_installed": "✗ Non installé",
"status_idle": "Cliquez sur 'Vérifier la Version' pour commencer",
"status_checking": "🔄 Vérification de la version...",
"check_updates": "Vérifier la Version",
"check_failed": "Échec de la vérification de la version. Vérifiez votre connexion Internet.",
"description": "Vérifiez votre version de FFmpeg et comparez-la avec la dernière version disponible.",
"guide_info": " Pour installer ou mettre à jour FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>cliquez ici pour consulter notre guide d'installation complet</a>."
},
"deno": {
"setup_required": "Configuration de Deno requise",
"setup_description": "YTSage nécessite Deno pour exécuter certaines fonctionnalités.<br><br>Deno n'a pas été trouvé dans le répertoire local de l'application. YTSage doit configurer Deno pour votre système {os_name}.<br><br>Cliquez sur 'Configurer Deno' pour télécharger et installer automatiquement.",
"setup_button": "Configurer Deno",
"downloading": "Téléchargement de Deno...",
"extracting": "Extraction de Deno...",
"verifying": "Vérification de Deno...",
"success": "Deno a été installé avec succès !",
"download_failed": "Échec du téléchargement",
"download_error": "Échec du téléchargement de Deno : {error}",
"verification_failed": "Échec de la vérification SHA256. Le fichier téléchargé peut être corrompu ou altéré.",
"setup_failed": "Échec de la configuration de Deno. Certaines fonctionnalités peuvent ne pas fonctionner correctement.",
"setup_error": "Erreur de configuration"
},
"deno_updater": {
"title": "Vérificateur et Mise à Jour de Version Deno",
"description": "Vérifiez votre version de Deno et mettez à jour vers la dernière version.",
"current_version": "Version Actuelle :",
"latest_version": "Dernière Version :",
"status_idle": "Cliquez sur 'Vérifier les Mises à Jour' pour commencer",
"status_checking": "🔄 Vérification des mises à jour...",
"status_up_to_date": "✓ À jour",
"status_update_available": "⚠ Mise à jour disponible",
"status_not_installed": "✗ Non installé",
"check_updates": "Vérifier les Mises à Jour",
"update_now": "Mettre à Jour Deno",
"updating": "🔄 Mise à jour de Deno...",
"update_success": "✅ Deno a été mis à jour avec succès !",
"update_failed": "❌ Échec de la mise à jour : {error}",
"check_failed": "Échec de la vérification des mises à jour. Veuillez vérifier votre connexion Internet."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "हिन्दी (Hindi)",
"select_language": "भाषा चुनें:",
"current_language": "वर्तमान भाषा: {language}",
"restart_required": "भाषा परिवर्तन एप्लिकेशन पुनरारंभ के बाद प्रभावी होगा।",
"english": "अंग्रेजी",
"spanish": "स्पेनिश",
"portuguese": "पुर्तगाली",
"russian": "रूसी",
"chinese": "चीनी",
"german": "जर्मन",
"french": "फ्रेंच",
"hindi": "हिन्दी",
"indonesian": "इंडोनेशियाई",
"turkish": "तुर्की",
"polish": "पोलिश",
"italian": "इतालवी",
"arabic": "अरबी",
"japanese": "जापानी",
"help_text": "इंटरफेस के लिए अपनी पसंदीदा भाषा चुनें।",
"restart_notice": "भाषा परिवर्तन एप्लिकेशन पुनरारंभ के बाद प्रभावी होगा।"
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "तैयार"
},
"formats": {
"show_formats": "प्रारूप दिखाएं:",
"video_format": "वीडियो प्रारूप:",
"audio_format": "ऑडियो प्रारूप:",
"no_formats": "कोई प्रारूप उपलब्ध नहीं",
"loading": "प्रारूप लोड हो रहे हैं...",
"select": "चुनें",
"quality": "गुणवत्ता",
"extension": "एक्सटेंशन",
"resolution": "रिज़ॉल्यूशन",
"file_size": "फ़ाइल आकार",
"codec": "कोडेक",
"audio": "ऑडियो",
"fps": "फ्रेम दर",
"hdr": "HDR",
"will_merge_audio": "ऑडियो मर्ज किया जाएगा",
"has_audio": "✓ ऑडियो है",
"audio_only": "केवल ऑडियो",
"best_4k": "सर्वोत्तम (4K)",
"best_2k": "सर्वोत्तम (2K)",
"high_1080p": "उच्च (1080p)",
"high_720p": "उच्च (720p)",
"medium_480p": "मध्यम (480p)",
"low_quality": "निम्न गुणवत्ता",
"best_audio": "सर्वोत्तम ऑडियो",
"high_audio": "उच्च ऑडियो",
"medium_audio": "मध्यम ऑडियो",
"low_audio": "निम्न ऑडियो",
"audio_only_resolution": "केवल ऑडियो"
},
"buttons": {
"reset": "रीसेट",
"download": "डाउनलोड",
"pause": "रोकें",
"resume": "जारी रखें",
"cancel": "रद्द करें",
"browse": "ब्राउज़",
"clear": "साफ़ करें",
"ok": "ठीक है",
"apply": "लागू करें",
"close": "बंद करें",
"run_command": "कमांड चलाएं",
"custom_command_help": "सहायता",
"about": "के बारे में",
"analyze": "विश्लेषण",
"paste_url": "URL पेस्ट करें",
"select_videos": "वीडियो चुनें...",
"video": "वीडियो",
"audio_only": "केवल ऑडियो",
"custom_options": "कस्टम विकल्प",
"trim_video": "वीडियो ट्रिम करें",
"download_settings": "डाउनलोड सेटिंग्स",
"update": "yt-dlp अपडेट करें",
"select_defaults": "डिफ़ॉल्ट चुनें",
"select_all": "सभी चुनें",
"deselect_all": "सभी को अचयनित करें",
"open_folder": "फ़ोल्डर स्थान खोलें",
"history": "इतिहास",
"save_playlist": "प्लेलिस्ट को इस रूप में सहेजें"
},
"dialogs": {
"custom_options": "कस्टम विकल्प",
"settings": "सेटिंग्स",
"select_folder": "डाउनलोड फ़ोल्डर चुनें",
"sponsorblock_categories": "SponsorBlock श्रेणियां",
"sponsorblock_description": "डाउनलोड के दौरान अपने आप हटाए जाने वाले वीडियो सेगमेंट के प्रकार चुनें।\nSponsorBlock इन सेगमेंट की पहचान के लिए समुदाय द्वारा प्रस्तुत डेटा का उपयोग करता है।",
"select_subtitles": "उपशीर्षک चुनें",
"filter_languages_placeholder": "भाषाएं फ़िल्टर करें (जैसे: hi, en)...",
"no_subtitles_available": "कोई उपशीर्षक उपलब्ध नहीं",
"matching": "मेल खाता",
"ytdlp_log_title": "yt-dlp लॉग",
"filter_playlist_placeholder": "वीडियो फ़िल्टर करें..."
},
"tabs": {
"cookies": "कुकीज़ के साथ लॉगिन",
"custom_command": "कस्टम कमांड",
"proxy": "प्रॉक्सी",
"language": "भाषा",
"updater": "अपडेटर"
},
"cookies": {
"help_text": "प्रमाणीकरण के लिए कुकीज़ प्रदान करने का तरीका चुनें।\nयह निजी वीडियो और उच्च गुणवत्ता वाली ऑडियो फ़ाइलें डाउनलोड करने की अनुमति देता है।",
"cookie_source": "कुकी स्रोत",
"use_cookie_file": "कुकी फ़ाइल का उपयोग करें",
"extract_from_browser": "ब्राउज़र से निकालें",
"recommended": "अनुशंसित",
"cookie_file": "कुकी फ़ाइल",
"cookie_file_placeholder": "cookies.txt फ़ाइल का पथ...",
"browser_selection": "ब्राउज़र चयन",
"browser_help": "वह ब्राउज़र चुनें जिससे कुकीज़ निकालनी हैं:",
"browser_label": "ब्राउज़र:",
"profile_label": "प्रोफ़ाइल:",
"profile_placeholder": "डिफ़ॉल्ट",
"browser_extract_message": "लागू करने पर ब्राउज़र कुकीज़ निकाली जाएंगी",
"file_selected_message": "कुकी फ़ाइल चुनी गई - लागू करने के लिए OK दबाएं",
"select_file_title": "कुकी फ़ाइल चुनें",
"file_filter": "कुकी फ़ाइलें (*.txt *.lwp)",
"file_selected_title": "कुकी फ़ाइल लागू की गई",
"file_applied_message": "कुकी फ़ाइल लागू की गई: {path}",
"browser_selected_title": "ब्राउज़र कुकीज़ लागू की गईं",
"browser_applied_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}",
"cleared_title": "कुकीज़ साफ़ की गईं",
"cleared_message": "कुकी सेटिंग्स साफ़ कर दी गई हैं",
"active_browser": "✓ सक्रिय: ब्राउज़र कुकीज़ ({browser})",
"active_file": "✓ सक्रिय: कुकी फ़ाइल ({file})",
"none_active": "○ कोई कुकी सक्रिय नहीं",
"remember_settings": "अगले स्टार्टअप पर कुकी सेटिंग्स याद रखें"
},
"custom_command": {
"help_text": "नीचे अपना कस्टम yt-dlp कमांड दर्ज करें। वर्तमान URL स्वचालित रूप से जोड़ा जाएगा।<br><br>विकल्पों की पूरी सूची और उपयोग के उदाहरणों के लिए <a href=\"{docs_url}\">आधिकारिक yt-dlp दस्तावेज़ देखने के लिए यहाँ क्लिक करें</a>।<br><br>नोट: डाउनलोड पथ और फ़ाइलनाम टेम्प्लेट स्वचालित रूप से संभाले जाते हैं।",
"input_label": "yt-dlp आर्गुमेंट्स:",
"input_placeholder": "यहाँ yt-dlp आर्गुमेंट्स दर्ज करें...\n\nउदाहरण: --extract-audio --audio-format mp3",
"output_label": "कमांड आउटपुट:",
"output_placeholder": "कमांड आउटपुट यहाँ दिखाई देगा...",
"command_placeholder": "कस्टम yt-dlp कमांड दर्ज करें...",
"command_help": "उपलब्ध प्लेसहोल्डर:\n{url} - वीडियो URL\n{output} - आउटपुट फ़ोल्डर\n\nउदाहरण: --write-info-json --write-thumbnail",
"full_command": "🔧 पूरा कमांड: {command}",
"command_success": "✅ कस्टम कमांड सफलतापूर्वक निष्पादित!",
"command_failed": "❌ कमांड असफल, एग्जिट कोड {code}",
"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": "डाउनलोड के लिए प्रॉक्सी सेटिंग्स कॉन्फ़िगर करें। प्रत्यक्ष कनेक्शन के लिए खाली छोड़ें।",
"main_proxy": "मुख्य प्रॉक्सी",
"main_proxy_help": "सभी डाउनलोड के लिए प्राथमिक प्रॉक्सी सर्वर। HTTP/HTTPS और SOCKS5 प्रोटोकॉल समर्थित।",
"proxy_url_label": "प्रॉक्सी URL:",
"proxy_url_placeholder": "http://proxy:port या socks5://proxy:port",
"proxy_examples": "उदाहरण:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "भू-बाईपास प्रॉक्सी",
"geo_proxy_help": "भौगोलिक प्रतिबंधों को बायपास करने के लिए द्वितीयक प्रॉक्सी।",
"geo_proxy_url_label": "भू-बाईपास प्रॉक्सी URL:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "मुख्य प्रॉक्सी साफ़ करें",
"clear_geo_proxy": "भू-प्रॉक्सी साफ़ करें",
"proxy_url": "प्रॉक्सी URL",
"proxy_placeholder": "http://proxy:port या socks5://proxy:port",
"geo_bypass": "भू-बाईपास प्रॉक्सी (भौगोलिक प्रतिबंधों के लिए)",
"geo_bypass_placeholder": "http://proxy:port भू-ब्लॉकिंग बायपास के लिए",
"invalid_main_url": "अमान्य मुख्य प्रॉक्सी URL प्रारूप",
"invalid_geo_url": "अमान्य भू-प्रॉक्सी URL प्रारूप",
"main_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": "डाउनलोड तैयार हो रहा है...",
"starting": "🚀 डाउनलोड शुरू हो रहा है...",
"fetching_info": "🔍 वीडियो जानकारी प्राप्त कर रहे हैं...",
"preparing_streams": "🎯 वीडियो स्ट्रीम तैयार हो रही है...",
"downloading_audio": "⏬ ऑडियो डाउनलोड हो रहा है...",
"downloading_video": "⏬ वीडियो डाउनलोड हो रहा है...",
"downloading_subtitle": "⏬ उपशीर्षक डाउनलोड हो रहे हैं...",
"downloading": "⏬ डाउनलोड हो रहा है...",
"completed": "✅ डाउनलोड पूर्ण!",
"video_completed": "✅ वीडियो डाउनलोड पूर्ण!",
"audio_completed": "✅ ऑडियो डाउनलोड पूर्ण!",
"subtitle_completed": "✅ उपशीर्षक डाउनलोड पूर्ण!",
"completed_cleaning": "✅ डाउनलोड पूर्ण! साफ़ कर रहे हैं...",
"cancelled": "डाउनलोड रद्द किया गया",
"processing_playlist": "📋 प्लेलिस्ट डेटा प्रोसेस हो रहा है...",
"paused": "डाउनलोड रोका गया",
"resumed": "डाउनलोड जारी रखा गया",
"merging_formats": "✨ पोस्ट-प्रोसेसिंग: फॉर्मेट मर्ज हो रहे हैं...",
"removing_sponsor_segments": "✨ पोस्ट-प्रोसेसिंग: स्पॉन्सर सेगमेंट हटाए जा रहे हैं...",
"speed": "गति",
"eta": "शेष समय",
"please_enter_url": "कृपया पहले URL दर्ज करें।",
"please_set_path": "कृपया पहले डाउनलोड पथ सेट करें।",
"please_enter_url_and_path": "कृपया URL दर्ज करें और डाउनलोड पथ सेट करें।",
"please_select_format": "कृपया पहले प्रारूप चुनें।",
"downloading_fallback": "⚡ डाउनलोड हो रहा है..."
},
"update": {
"title": "yt-dlp अपडेट करें",
"checking": "अपडेट की जांच हो रही है...",
"update_available": "अपडेट उपलब्ध!\nवर्तमान संस्करण: {current}\nनवीनतम संस्करण: {latest}",
"up_to_date": "yt-dlp अप टू डेट है (संस्करण {version})",
"could_not_determine": "संस्करण निर्धारित नहीं हो सके।",
"error_comparing": "संस्करण तुलना में त्रुटि: {error}",
"update_available_failed": "अपडेट उपलब्ध! (तुलना असफल)\nवर्तमान: {current}\nनवीनतम: {latest}",
"updating": "अपडेट हो रहा है...",
"initializing": "🚀 अपडेट प्रक्रिया प्रारंभ हो रही है...",
"checking_current": "🔍 वर्तमान इंस्टॉलेशन की जांच हो रही है...",
"found_at": "📍 yt-dlp मिला: {path}",
"error_getting_path": "❌ yt-dlp पथ प्राप्त करने में त्रुटि: {error}",
"updating_binary": "📦 ऐप-प्रबंधित yt-dlp बाइनरी अपडेट हो रही है...",
"update_failed": "❌ yt-dlp अपडेट असफल। कृपया पुनः प्रयास करें या अपना इंटरनेट कनेक्शन जांचें।",
"binary_updated": "✅ बाइनरी सफलतापूर्वक अपडेट हुई!",
"update_failed_stderr": "❌ yt-dlp अपडेट असफल: {error}",
"update_timeout": "❌ yt-dlp अपडेट टाइमआउट।",
"unexpected_error": "❌ अपडेट के दौरान अनपेक्षित त्रुटि: {error}",
"already_up_to_date": "✅ yt-dlp पहले से अप टू डेट है!",
"update_success": "✅ yt-dlp सफलतापूर्वक अपडेट किया गया है!",
"already_latest": "yt-dlp अप टू डेट है (संस्करण {version})",
"network_error": "❌ अपडेट के दौरान नेटवर्क त्रुटि: {error}",
"general_error": "❌ अपडेट असफल: {error}",
"update_in_progress_title": "अपडेट जारी है",
"update_in_progress_message": "yt-dlp अभी अपडेट हो रहा है। कृपया कुछ देर प्रतीक्षा करें।"
},
"about": {
"title": "YTSage के बारे में",
"version": "संस्करण {version}",
"description": "साफ PySide6 इंटरफेस के साथ आधुनिक YouTube डाउनलोडर।",
"author": "द्वारा: {author}",
"github": "GitHub: {repo}",
"system_info": "सिस्टम जानकारी",
"loading": "🔄 सिस्टम जानकारी लोड हो रही है...",
"refresh": "🔄",
"open_logs": "📂 लॉग्स",
"logs_tooltip": "एप्लिकेशन लॉग फ़ोल्डर खोलें",
"refreshing": "🔄 रिफ्रेश हो रहा है...",
"refresh_failed": "रिफ्रेश असफल",
"refresh_failed_message": "संस्करण जानकारी रिफ्रेश करने में असमर्थ।",
"detected": "✓ पता चला",
"missing": "✗ गुम",
"not_available": "उपलब्ध नहीं"
},
"time_range": {
"title": "वीडियो ट्रिम करें",
"time_range_group": "समय सीमा",
"start_time": "प्रारंभ समय (HH:MM:SS)",
"end_time": "समाप्ति समय (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "प्रारंभ समय (HH:MM:SS)",
"end_time_placeholder": "समाप्ति समय (HH:MM:SS)",
"force_keyframes": "कट पॉइंट्स पर कीफ्रेम्स को बाध्य करें",
"help_text": "वीडियो ट्रिम करने के लिए प्रारंभ और समाप्ति समय सेट करें।\nपूरा वीडियो डाउनलोड करने के लिए खाली छोड़ें।",
"invalid_format": "अमान्य समय प्रारूप। HH:MM:SS प्रारूप का उपयोग करें।",
"start_after_end": "प्रारंभ समय समाप्ति समय के बाद नहीं हो सकता।"
},
"settings": {
"title": "डाउनलोड सेटिंग्स",
"download_path": "डाउनलोड पथ",
"browse": "ब्राउज़ करें...",
"speed_limit": "गति सीमा",
"speed_limit_placeholder": "कोई नहीं",
"generic_mode": "जेनेरिक मोड",
"enable_generic_mode": "जेनेरिक मोड सक्षम करें (गैर-YouTube साइटों के लिए समर्थन)",
"generic_mode_help": "Dailymotion, CBC Gem और yt-dlp द्वारा समर्थित अन्य साइटों से डाउनलोड की अनुमति देता है।",
"auto_update_ytdlp": "yt-dlp स्वचालित अपडेट",
"enable_auto_updates": "yt-dlp स्वचालित अपडेट सक्षम करें",
"update_frequency": "अपडेट आवृत्ति:",
"check_startup": "हर स्टार्टअप पर जांचें (जांच के बीच कम से कम 1 घंटा)",
"check_daily": "दैनिक जांचें",
"check_weekly": "साप्ताहिक जांचें",
"check_updates_now": "yt-dlp अपडेट करें",
"update_check_title": "अपडेट जांच",
"could_not_determine_version": "वर्तमान yt-dlp संस्करण निर्धारित नहीं हो सका।",
"update_available_dialog": "अपडेट उपलब्ध!\n\nवर्तमान: {current}\nनवीनतम: {latest}\n\nअपडेट जारी रखने के लिए OK पर क्लिक करें।",
"up_to_date_dialog": "yt-dlp अप टू डेट है!\n\nवर्तमान संस्करण: {version}",
"error_checking_updates": "अपडेट जांचने में त्रुटि: {error}",
"settings_saved_title": "सेटिंग्स सेव की गईं",
"settings_saved_message": "स्वचालित अपडेट सेटिंग्स सफलतापूर्वक सेव हुईं!",
"error_title": "त्रुटि",
"failed_save_settings": "स्वचालित अपडेट सेटिंग्स सेव करना असफल।",
"error_saving_settings": "स्वचालित अपडेट सेटिंग्स सेव करने में त्रुटि: {error}",
"ytdlp_channel": "yt-dlp रिलीज़ चैनल",
"ytdlp_channel_stable": "स्थिर (परीक्षित रिलीज़)",
"ytdlp_channel_nightly": "नाइटली (दैनिक अपडेट, yt-dlp द्वारा अनुशंसित)",
"ytdlp_channel_description": "स्थिर और नाइटली रिलीज़ के बीच चुनें। नाइटली बिल्ड नवीनतम फिक्स और सुविधाओं के साथ दैनिक अपडेट होते हैं। आप किसी भी समय चैनल बदल सकते हैं।",
"ytdlp_switching_channel": "{channel} चैनल पर स्विच किया जा रहा है...",
"ytdlp_channel_switched": "✅ सफलतापूर्वक {channel} चैनल पर स्विच किया गया!",
"ytdlp_channel_switch_failed": "❌ चैनल स्विच करना विफल: {error}",
"ytdlp_current_channel": "वर्तमान चैनल: {channel}",
"app_updates_title": "YTSage अपडेट",
"check_app_updates": "स्टार्टअप पर YTSage अपडेट जांचें",
"check_beta_updates": "बीटा अपडेट प्राप्त करें",
"auto_update_title": "स्वचालित अपडेट सेटिंग्स",
"auto_update_header": "🔄 स्वचालित अपडेट सेटिंग्स",
"auto_update_description": "yt-dlp के लिए स्वचालित अपडेट कॉन्फ़िगर करें ताकि आपके पास हमेशा नवीनतम सुविधाएं और बग फिक्स हों।",
"current_status": "वर्तमान स्थिति",
"current_version_label": "वर्तमान yt-dlp संस्करण: जांच रहे हैं...",
"last_check_label": "अंतिम अपडेट जांच: कभी नहीं",
"next_check_label": "अगली जांच: सेटिंग्स के आधार पर",
"manual_check_button": "🔍 अभी अपडेट की जांच करें",
"update_frequency_group": "अपडेट आवृत्ति",
"save_settings": "सेटिंग्स सेव करें",
"settings_saved_successfully": "✅ सेटिंग्स सफलतापूर्वक सेव हुईं!",
"error_saving": "❌ सेटिंग्स सेव करने में त्रुटि: {error}",
"output_format_settings": "आउटपुट प्रारूप सेटिंग्स",
"force_output_format": "मर्ज करते समय आउटपुट प्रारूप बाध्य करें",
"preferred_format": "पसंदीदा प्रारूप:",
"force_format_help": "सक्षम होने पर, मर्ज किए गए वीडियो आपके पसंदीदा प्रारूप में परिवर्तित हो जाएंगे। yt-dlp को स्वचालित रूप से निर्णय लेने देने के लिए अक्षम करें।",
"format_mp4": "MP4 (सबसे संगत)",
"format_webm": "WebM (आधुनिक, खुला)",
"format_mkv": "MKV (सुविधा समृद्ध)",
"audio_format_settings": "ऑडियो प्रारूप सेटिंग्स",
"force_audio_format": "केवल ऑडियो डाउनलोड के लिए ऑडियो प्रारूप लागू करें",
"audio_normalization": "ऑडियो सामान्यीकरण (EBU R128)",
"audio_normalization_help": "सक्षम होने पर, ऑडियो ट्रैक सामान्य किए जाएंगे। ध्यान दें: इसके लिए री-एनकोडिंग की आवश्यकता होती है, इसलिए एक विशिष्ट ऑडियो प्रारूप (जैसे MP3 या M4A) को बाध्य किया जाना चाहिए।",
"preferred_audio_format": "पसंदीदा ऑडियो प्रारूप:",
"force_audio_format_help": "सक्षम होने पर, केवल ऑडियो डाउनलोड आपके पसंदीदा प्रारूप में परिवर्तित हो जाएंगे। यह केवल ऑडियो प्रारूप डाउनलोड करते समय लागू होता है।",
"audio_format_best": "सर्वोत्तम (कोई परिवर्तन नहीं)",
"audio_format_aac": "AAC (संपादन के लिए अच्छा)",
"audio_format_mp3": "MP3 (सार्वभौमिक)",
"audio_format_flac": "FLAC (हानिरहित)",
"audio_format_wav": "WAV (असंकुचित)",
"audio_format_opus": "Opus (कुशल)",
"audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (खुला)",
"filename_format": "आउटपुट फ़ाइलनाम प्रारूप",
"filename_format_help": "उपलब्ध चर: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. मानक yt-dlp आउटपुट टेम्प्लेट सिंटैक्स समर्थित है.",
"tab_general": "सामान्य",
"tab_format": "प्रारूप",
"tab_file": "फ़ाइल",
"concurrent_fragments": "एक साथ कनेक्शन",
"concurrent_fragments_help": "प्रति डाउनलोड कनेक्शन की संख्या। उच्च मान थ्रॉटलिंग को बायपास करते हैं लेकिन बहुत अधिक सेट होने पर अस्थायी ब्लॉक का कारण बन सकते हैं। डिफ़ॉल्ट: 1.",
"defaults_settings": "डिफ़ॉल्ट चयन सेटिंग्स",
"default_video_quality": "डिफ़ॉल्ट वीडियो रिज़ॉल्यूशन (ऊंचाई):",
"default_subtitle_language": "डिफ़ॉल्ट उपशीर्षक भाषा(ए):",
"defaults_help": "अपनी पसंदीदा वीडियो ऊंचाई और उपशीर्षक भाषाएं (कॉमा से अलग) सेट करें। उपलब्ध होने पर वे स्वतः चयनित हो जाएंगी।"
},
"main_ui": {
"url_placeholder": "YouTube वीडियो या प्लेलिस्ट URL दर्ज करें",
"url_placeholder_generic": "किसी भी समर्थित साइट से वीडियो या प्लेलिस्ट URL दर्ज करें",
"merge_subtitles": "उपशीर्षक मर्ज करें",
"save_thumbnail": "थंबनेल सेव करें",
"save_description": "विवरण सेव करें",
"embed_chapters": "चैप्टर एम्बेड करें",
"subtitles_selected": "{count} चुना गया",
"all_selected": "सभी चुने गए",
"select_videos_all": "वीडियो चुनें... (सभी चुने गए)",
"please_enter_url": "कृपया पहले URL दर्ज करें",
"error_title": "त्रुटि",
"cookie_file_selected_title": "कुकी फ़ाइल चुनी गई",
"cookie_file_selected_message": "कुकी फ़ाइल चुनी गई: {path}",
"browser_cookies_selected_title": "ब्राउज़र कुकीज़ चुनी गईं",
"browser_cookies_selected_message": "ब्राउज़र कुकीज़ निकाली जाएंगी: {browser}",
"error_no_format_info": "त्रुटि: कोई प्रारूप जानकारी उपलब्ध नहीं।",
"error_extract_info": "त्रुटि: बुनियादी वीडियो जानकारी निकालने में असमर्थ। कृपया अपना लिंक जांचें।",
"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": "जानकारी निकाली जा रही है...",
"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": "कृपया पहले उपशीर्षक चुनें",
"settings_tooltip": "वर्तमान पथ: {path}\nस्पीड लिमिट: {speed_limit}",
"speed_limit_none": "कोई नहीं",
"open_folder_error": "फ़ोल्डर नहीं खुल सका: {error}",
"time_range_set": "सेक्शन सेट: {section}"
},
"sponsorblock": {
"sponsor": "प्रायोजक",
"sponsor_desc": "पेड विज्ञापन, पेड सिफारिशें और प्रत्यक्ष विज्ञापन",
"selfpromo": "अवैतनिक/स्व-प्रचार",
"selfpromo_desc": "निर्माता की अपनी सामग्री के लिए अवैतनिक प्रचार",
"interaction": "इंटरैक्शन रिमाइंडर",
"interaction_desc": "दर्शकों से लाइक, सब्स्क्राइब या सोशल मीडिया फॉलो करने के लिए कहा जाता है",
"intro": "परिचय",
"intro_desc": "वीडियो परिचय जिसे छोड़ा जा सकता है",
"outro": "आउट्रो/एंड कार्ड",
"outro_desc": "क्रेडिट या जब वीडियो समाप्त होता है",
"preview": "पूर्वावलोकन/सारांश",
"preview_desc": "पिछले वीडियो का संक्षिप्त सारांश या आगामी सामग्री का पूर्वावलोकन",
"music_offtopic": "गैर-संगीत खंड",
"music_offtopic_desc": "केवल संगीत वीडियो के लिए। गैर-संगीत खंडों को चिह्नित करता है",
"filler": "फिलर टैंजेंट",
"filler_desc": "केवल फिलर या हास्य के लिए जोड़े गए विचलन दृश्य"
},
"video_info": {
"channel": "चैनल",
"views": "👁 दृश्य",
"likes": "👍 पसंद",
"upload_date": "📅 अपलोड दिनांक",
"duration": "⏱ अवधि",
"unknown_channel": "अज्ञात चैनल",
"unknown_date": "अज्ञात दिनांक",
"unknown_title": "अज्ञात शीर्षक"
},
"command": {
"running": "चल रहा है...",
"run_command": "कमांड चलाएं"
},
"selection": {
"none_selected": "0 चुना गया",
"one_selected": "1 श्रेणी चुनी गई",
"count_selected": "{count} चुने गए"
},
"status": {
"ready": "तैयार",
"file_exists": "⚠️ फ़ाइल पहले से मौजूद है",
"video_file_exists": "⚠️ वीडियो फ़ाइल पहले से मौजूद है",
"audio_file_exists": "⚠️ ऑडियो फ़ाइल पहले से मौजूद है",
"subtitle_file_exists": "⚠️ उपशीर्षक फ़ाइल पहले से मौजूद है",
"cancelling": "डाउनलोड रद्द हो रहा है...",
"thumbnail_saved": "✅ थंबनेल सहेजा गया: {filename}",
"thumbnail_error": "❌ थंबनेल त्रुटि: {error}",
"thumbnail_no_image": "सहेजने के लिए कोई थंबनेल उपलब्ध नहीं"
},
"errors": {
"playlist_no_videos": "त्रुटि: प्लेलिस्ट में कोई वैध वीडियो नहीं है।",
"playlist_no_url": "त्रुटि: प्लेलिस्ट के पहले वीडियो के लिए URL प्राप्त नहीं कर सके।",
"ytdlp_not_found": "त्रुटि: yt-dlp executable नहीं मिला। कृपया पहले yt-dlp इंस्टॉल करें।",
"ytdlp_not_found_path": "त्रुटि: yt-dlp executable नहीं मिला। यह अनुचित इंस्टॉलेशन या PATH समस्या के कारण हो सकता है।",
"no_data_returned": "त्रुटि: yt-dlp से कोई डेटा वापस नहीं आया",
"no_format_info": "त्रुटि: कोई प्रारूप जानकारी उपलब्ध नहीं।",
"analysis_timeout": "त्रुटि: विश्लेषण टाइमआउट। कृपया पुनः प्रयास करें।",
"invalid_speed_limit": "❌ त्रुटि: सेटिंग्स में अमान्य गति सीमा मान।",
"ytdlp_failed": "त्रुटि: yt-dlp असफल: {error}",
"parse_failed": "त्रुटि: yt-dlp आउटपुट पार्स करने में विफल: {error}",
"analysis_failed": "त्रुटि: विश्लेषण विफल: {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}",
"private_video": "यह वीडियो निजी हो सकता है। कृपया कस्टम विकल्पों से कुकीज़ का उपयोग करें।"
},
"update_dialog": {
"title": "अपडेट उपलब्ध",
"new_version_available": "YTSage का नया संस्करण उपलब्ध है!",
"current_version_label": "वर्तमान संस्करण:",
"latest_version_label": "नवीनतम संस्करण:",
"changelog": "परिवर्तन सूची",
"download_update": "अपडेट डाउनलोड करें",
"remind_later": "बाद में याद दिलाएं"
},
"playlist": {
"unknown": "अज्ञात प्लेलिस्ट",
"total_videos": "कुल वीडियो: {count}",
"display_format": "प्लेलिस्ट: {title} | {count} वीडियो",
"select_videos_title": "प्लेलिस्ट वीडियो चुनें",
"save_as": "प्लेलिस्ट को इस रूप में सहेजें",
"save_success_title": "सफलता",
"saved_successfully": "प्लेलिस्ट सफलतापूर्वक सहेजी गई।",
"save_error_title": "सहेजने में त्रुटि",
"no_videos_to_save": "कोई प्लेलिस्ट प्रविष्टियाँ एकत्रित नहीं हुई!",
"save_error_msg": "प्लेलिस्ट सहेजने में विफल।"
},
"subtitle_selection": {
"count_selected": "{count} चुना गया"
},
"file_exists_dialog": {
"title": "फ़ाइल पहले से मौजूद है",
"message": "फ़ाइल पहले से मौजूद है:\n{filename}",
"info": "यह वीडियो पहले ही डाउनलोड किया जा चुका है।"
},
"auto_update": {
"last_check_never": "अंतिम अपडेट जांच: कभी नहीं",
"last_check": "अंतिम अपडेट जांच: {time}",
"next_check_disabled": "अगली जांच: अक्षम",
"next_check_startup": "अगली जांच: स्टार्टअप पर",
"next_check_overdue": "अगली जांच: अभी (विलंबित)",
"next_check": "अगली जांच: {time}",
"next_check_error": "अगली जांच: गणना त्रुटि",
"checking": "🔄 जांच रहे हैं...",
"check_now": "🔍 अभी अपडेट की जांच करें",
"current_version": "वर्तमान yt-dlp संस्करण: {version}"
},
"url_validation": {
"empty_url": "URL खाली नहीं हो सकता",
"invalid_format": "अमान्य URL प्रारूप",
"invalid_scheme": "URL http:// या https:// से शुरू होना चाहिए",
"missing_domain": "अमान्य URL: डोमेन नाम गायब है",
"unsupported_platform": "YTSage केवल YouTube और YouTube Music URLs का समर्थन करता है।\nडोमेन '{domain}' समर्थित नहीं है।",
"invalid_youtu_be": "अमान्य youtu.be URL: वीडियो ID गायब है"
},
"ytdlp_errors": {
"private_video": "यह एक निजी वीडियो है। आप कुकीज़ का उपयोग करके अपने खाते में लॉग इन करके इसे डाउनलोड कर सकते हैं।\nप्रमाणित करने के लिए 'कस्टम विकल्प' → 'कुकीज़ के साथ लॉगिन' → 'ब्राउज़र से कुकीज़ निकालें' पर जाएं।",
"age_restricted": "यह वीडियो आयु-प्रतिबंधित है। इसे एक्सेस करने के लिए आपको लॉग इन करना होगा।\nअपने खाते से प्रमाणित करने के लिए 'कस्टम विकल्प' → 'कुकीज़ के साथ लॉगिन' का उपयोग करें।",
"geo_blocked": "यह वीडियो आपके क्षेत्र में उपलब्ध नहीं है (भू-अवरोधित)।\nआपको VPN का उपयोग करने की आवश्यकता हो सकती है या वीडियो आपके देश में प्रतिबंधित हो सकता है।",
"video_unavailable": "यह वीडियो हटा दिया गया है या अब उपलब्ध नहीं है।\nवीडियो को अपलोडर द्वारा हटाया जा सकता है या नीति उल्लंघन के कारण हटाया जा सकता है।",
"live_stream": "यह एक लाइव स्ट्रीम है जिसे सक्रिय रहते हुए डाउनलोड नहीं किया जा सकता।\nस्ट्रीम समाप्त होने तक प्रतीक्षा करें, फिर संग्रहीत संस्करण डाउनलोड करने का प्रयास करें।",
"playlist_error": "इस प्लेलिस्ट तक पहुंचने में असमर्थ। यह निजी, हटाई गई या खाली हो सकती है।\nजांचें कि प्लेलिस्ट मौजूद है और सार्वजनिक रूप से सुलभ है।",
"network_error": "नेटवर्क कनेक्शन त्रुटि। कृपया अपना इंटरनेट कनेक्शन जांचें और पुनः प्रयास करें।\nयदि समस्या बनी रहती है, तो वीडियो सर्वर अस्थायी रूप से अनुपलब्ध हो सकता है।",
"invalid_url": "अमान्य या असमर्थित URL। कृपया लिंक जांचें और पुनः प्रयास करें।\nसुनिश्चित करें कि आप एक मान्य YouTube, Vimeo, या अन्य समर्थित प्लेटफ़ॉर्म URL का उपयोग कर रहे हैं।",
"premium_content": "इस सामग्री के लिए YouTube Premium या चैनल सदस्यता की आवश्यकता है।\nआपको ऐसे खाते से लॉग इन करना होगा जिसके पास इस सामग्री तक पहुंच है।",
"copyright_blocked": "कॉपीराइट दावों के कारण यह वीडियो अवरुद्ध है।\nसामग्री स्वामी ने इस वीडियो तक पहुंच प्रतिबंधित कर दी है।",
"extraction_failed": "वीडियो जानकारी निकालने में विफल। यह एक अस्थायी समस्या हो सकती है।\nकृपया कुछ मिनट में पुनः प्रयास करें, या जांचें कि वीडियो लिंक सही है या नहीं।",
"generic_error": "वीडियो जानकारी निकालने में विफल। कृपया अपना लिंक जांचें।\nतकनीकी विवरण: {error}"
},
"history": {
"title": "डाउनलोड इतिहास",
"clear_all": "सभी साफ़ करें",
"clear_confirm_title": "इतिहास साफ़ करें?",
"clear_confirm_message": "क्या आप निश्चित हैं? इसे पूर्ववत नहीं किया जा सकता।",
"no_history": "अभी तक कोई इतिहास नहीं",
"no_history_description": "आपके डाउनलोड यहां दिखाई देंगे",
"loading": "इतिहास लोड हो रहा है...",
"search_placeholder": "खोजें...",
"open_location": "स्थान खोलें",
"redownload": "फिर से डाउनलोड करें",
"remove": "हटाएं",
"file_not_found": "फ़ाइल नहीं मिली",
"file_not_found_message": "फ़ाइल स्थानांतरित या हटाई गई:\n{path}",
"downloaded_on": "डाउनलोड किया: {date}",
"file_size": "आकार: {size}",
"audio_download": "ऑडियो",
"video_download": "वीडियो",
"remove_confirm_title": "हटाएं?",
"remove_confirm_message": "इतिहास से हटाएं?\n\n{title}",
"item_removed": "हटाया गया",
"history_cleared": "इतिहास साफ़ किया गया",
"entries_count": "{count} डाउनलोड",
"one_entry": "1 डाउनलोड",
"redownload_confirm_title": "फिर से डाउनलोड करें?",
"redownload_confirm_message": "फिर से डाउनलोड करें?\n\n{title}",
"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 संस्करण जांचकर्ता",
"current_version": "वर्तमान संस्करण:",
"latest_version": "नवीनतम संस्करण:",
"status_up_to_date": "✓ अप टू डेट",
"status_update_available": "⚠ अपडेट उपलब्ध",
"status_not_installed": "✗ इंस्टॉल नहीं है",
"status_idle": "शुरू करने के लिए 'संस्करण जांचें' पर क्लिक करें",
"status_checking": "🔄 संस्करण जांच रहे हैं...",
"check_updates": "संस्करण जांचें",
"check_failed": "संस्करण जांच विफल रही। कृपया अपना इंटरनेट कनेक्शन जांचें।",
"description": "अपने FFmpeg संस्करण की जांच करें और इसे नवीनतम उपलब्ध संस्करण से तुलना करें।",
"guide_info": " FFmpeg को इंस्टॉल या अपडेट करने के लिए, <a href='https://github.com/oop7/ffmpeg-install-guide'>हमारी व्यापक इंस्टॉलेशन गाइड देखने के लिए यहां क्लिक करें</a>."
},
"deno": {
"setup_required": "Deno सेटअप आवश्यक",
"setup_description": "YTSage को कुछ विशेषताओं को चलाने के लिए Deno की आवश्यकता है।<br><br>Deno एप की स्थानीय निर्देशिका में नहीं मिला। YTSage को आपकी {os_name} प्रणाली के लिए Deno सेटअप करने की आवश्यकता है।<br><br>स्वचालित रूप से डाउनलोड और इंस्टॉल करने के लिए 'Deno सेटअप करें' पर क्लिक करें।",
"setup_button": "Deno सेटअप करें",
"downloading": "Deno डाउनलोड हो रहा है...",
"extracting": "Deno निकाला जा रहा है...",
"verifying": "Deno की जांच हो रही है...",
"success": "Deno सफलतापूर्वक स्थापित हो गया!",
"download_failed": "डाउनलोड विफल",
"download_error": "Deno डाउनलोड करने में विफल: {error}",
"verification_failed": "SHA256 सत्यापन विफल। डाउनलोड की गई फ़ाइल दूषित या छेड़छाड़ की गई हो सकती है।",
"setup_failed": "Deno सेटअप करने में विफल। कुछ विशेषताएं सही ढंग से काम नहीं कर सकती हैं।",
"setup_error": "सेटअप त्रुटि"
},
"deno_updater": {
"title": "Deno संस्करण जांचकर्ता और अपडेटर",
"description": "अपने Deno संस्करण की जांच करें और नवीनतम संस्करण में अपडेट करें।",
"current_version": "वर्तमान संस्करण:",
"latest_version": "नवीनतम संस्करण:",
"status_idle": "शुरू करने के लिए 'अपडेट की जांच करें' पर क्लिक करें",
"status_checking": "🔄 अपडेट की जांच हो रही है...",
"status_up_to_date": "✓ अपडेट है",
"status_update_available": "⚠ अपडेट उपलब्ध है",
"status_not_installed": "✗ स्थापित नहीं है",
"check_updates": "अपडेट की जांच करें",
"update_now": "Deno अपडेट करें",
"updating": "🔄 Deno अपडेट हो रहा है...",
"update_success": "✅ Deno सफलतापूर्वक अपडेट हो गया है!",
"update_failed": "❌ अपडेट विफल: {error}",
"check_failed": "अपडेट की जांच विफल। कृपया अपना इंटरनेट कनेक्शन जांचें।"
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Bahasa Indonesia (Indonesian)",
"select_language": "Pilih Bahasa:",
"current_language": "Bahasa saat ini: {language}",
"restart_required": "Perubahan bahasa akan berlaku setelah memulai ulang aplikasi.",
"english": "Bahasa Inggris",
"spanish": "Bahasa Spanyol",
"portuguese": "Bahasa Portugis",
"russian": "Bahasa Rusia",
"chinese": "Bahasa Mandarin",
"german": "Bahasa Jerman",
"french": "Bahasa Prancis",
"hindi": "Bahasa Hindi",
"indonesian": "Bahasa Indonesia",
"turkish": "Bahasa Turki",
"polish": "Bahasa Polandia",
"italian": "Bahasa Italia",
"arabic": "Bahasa Arab",
"japanese": "Bahasa Jepang",
"help_text": "Pilih bahasa yang diinginkan untuk antarmuka.",
"restart_notice": "Perubahan bahasa akan berlaku setelah memulai ulang aplikasi."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Siap"
},
"formats": {
"show_formats": "Tampilkan format:",
"video_format": "Format video:",
"audio_format": "Format audio:",
"no_formats": "Tidak ada format yang tersedia",
"loading": "Memuat format...",
"select": "Pilih",
"quality": "Kualitas",
"extension": "Ekstensi",
"resolution": "Resolusi",
"file_size": "Ukuran file",
"codec": "Codec",
"audio": "Audio",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "Audio akan digabungkan",
"has_audio": "✓ Memiliki audio",
"audio_only": "Audio saja",
"best_4k": "Terbaik (4K)",
"best_2k": "Terbaik (2K)",
"high_1080p": "Tinggi (1080p)",
"high_720p": "Tinggi (720p)",
"medium_480p": "Sedang (480p)",
"low_quality": "Kualitas rendah",
"best_audio": "Audio terbaik",
"high_audio": "Audio tinggi",
"medium_audio": "Audio sedang",
"low_audio": "Audio rendah",
"audio_only_resolution": "Audio saja"
},
"buttons": {
"reset": "Atur Ulang",
"download": "Unduh",
"pause": "Jeda",
"resume": "Lanjutkan",
"cancel": "Batal",
"browse": "Jelajahi",
"clear": "Bersihkan",
"ok": "OK",
"apply": "Terapkan",
"close": "Tutup",
"run_command": "Jalankan perintah",
"custom_command_help": "Bantuan",
"about": "Tentang",
"analyze": "Analisis",
"paste_url": "Tempel URL",
"select_videos": "Pilih video...",
"video": "Video",
"audio_only": "Audio saja",
"custom_options": "Opsi khusus",
"trim_video": "Potong video",
"download_settings": "Pengaturan unduhan",
"update": "Perbarui yt-dlp",
"select_defaults": "Pilih default",
"select_all": "Pilih semua",
"deselect_all": "Batalkan pilihan semua",
"open_folder": "Buka lokasi folder",
"history": "Riwayat",
"save_playlist": "Simpan Playlist Sebagai"
},
"dialogs": {
"custom_options": "Opsi khusus",
"settings": "Pengaturan",
"select_folder": "Pilih folder unduhan",
"sponsorblock_categories": "Kategori SponsorBlock",
"sponsorblock_description": "Pilih jenis segmen video yang akan dihapus secara otomatis selama pengunduhan.\nSponsorBlock menggunakan data yang dikirimkan komunitas untuk mengidentifikasi segmen ini.",
"select_subtitles": "Pilih subtitle",
"filter_languages_placeholder": "Filter bahasa (misalnya: id, en)...",
"no_subtitles_available": "Tidak ada subtitle yang tersedia",
"matching": "yang cocok",
"ytdlp_log_title": "Log yt-dlp",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Masuk dengan cookies",
"custom_command": "Perintah khusus",
"proxy": "Proxy",
"language": "Bahasa",
"updater": "Pembaruan"
},
"cookies": {
"help_text": "Pilih cara memberikan cookies untuk autentikasi.\nIni memungkinkan pengunduhan video pribadi dan file audio berkualitas tinggi.",
"cookie_source": "Sumber cookie",
"use_cookie_file": "Gunakan file cookie",
"extract_from_browser": "Ekstrak dari browser",
"recommended": "Direkomendasikan",
"cookie_file": "File cookie",
"cookie_file_placeholder": "Jalur ke file cookies.txt...",
"browser_selection": "Pilihan browser",
"browser_help": "Pilih browser untuk mengekstrak cookies:",
"browser_label": "Browser:",
"profile_label": "Profil:",
"profile_placeholder": "Default",
"browser_extract_message": "Cookies browser akan diekstrak saat diterapkan",
"file_selected_message": "File cookie dipilih - Klik OK untuk menerapkan",
"select_file_title": "Pilih file cookie",
"file_filter": "File cookie (*.txt *.lwp)",
"file_selected_title": "File Cookie Diterapkan",
"file_applied_message": "File cookie diterapkan: {path}",
"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",
"active_browser": "✓ Aktif: cookie browser ({browser})",
"active_file": "✓ Aktif: file cookie ({file})",
"none_active": "○ Tidak ada cookie aktif",
"remember_settings": "Ingat pengaturan cookie pada startup berikutnya"
},
"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.",
"input_label": "Argumen yt-dlp:",
"input_placeholder": "Masukkan argumen yt-dlp di sini...\n\nContoh: --extract-audio --audio-format mp3",
"output_label": "Output perintah:",
"output_placeholder": "Output perintah akan muncul di sini...",
"command_placeholder": "Masukkan perintah yt-dlp khusus...",
"command_help": "Placeholder yang tersedia:\n{url} - URL video\n{output} - Folder output\n\nContoh: --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Proxy utama",
"main_proxy_help": "Server proxy utama untuk semua unduhan. Mendukung protokol HTTP/HTTPS dan SOCKS5.",
"proxy_url_label": "URL Proxy:",
"proxy_url_placeholder": "http://proxy:port atau socks5://proxy:port",
"proxy_examples": "Contoh:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Proxy bypass geografis",
"geo_proxy_help": "Proxy sekunder khusus untuk mem-bypass pembatasan geografis.",
"geo_proxy_url_label": "URL proxy bypass geografis:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Hapus proxy utama",
"clear_geo_proxy": "Hapus proxy geografis",
"proxy_url": "URL Proxy",
"proxy_placeholder": "http://proxy:port atau socks5://proxy:port",
"geo_bypass": "Proxy bypass geografis (untuk pembatasan geografis)",
"geo_bypass_placeholder": "http://proxy:port untuk mem-bypass geo-blocking",
"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",
"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...",
"starting": "🚀 Memulai unduhan...",
"fetching_info": "🔍 Mengambil informasi video...",
"preparing_streams": "🎯 Mempersiapkan aliran video...",
"downloading_audio": "⏬ Mengunduh audio...",
"downloading_video": "⏬ Mengunduh video...",
"downloading_subtitle": "⏬ Mengunduh subtitle...",
"downloading": "⏬ Mengunduh...",
"completed": "✅ Unduhan selesai!",
"video_completed": "✅ Unduhan video selesai!",
"audio_completed": "✅ Unduhan audio selesai!",
"subtitle_completed": "✅ Unduhan subtitle selesai!",
"completed_cleaning": "✅ Unduhan selesai! Membersihkan...",
"cancelled": "Unduhan dibatalkan",
"processing_playlist": "📋 Memproses data playlist...",
"paused": "Unduhan dijeda",
"resumed": "Unduhan dilanjutkan",
"merging_formats": "✨ Pasca-pemrosesan: Menggabungkan format...",
"removing_sponsor_segments": "✨ Pasca-pemrosesan: Menghapus segmen sponsor...",
"speed": "Kecepatan",
"eta": "Waktu tersisa",
"please_enter_url": "Silakan masukkan URL terlebih dahulu.",
"please_set_path": "Silakan atur jalur unduhan terlebih dahulu.",
"please_enter_url_and_path": "Silakan masukkan URL dan atur jalur unduhan.",
"please_select_format": "Silakan pilih format terlebih dahulu.",
"downloading_fallback": "⚡ Mengunduh..."
},
"update": {
"title": "Perbarui yt-dlp",
"checking": "Memeriksa pembaruan...",
"update_available": "Pembaruan tersedia!\nVersi saat ini: {current}\nVersi terbaru: {latest}",
"up_to_date": "yt-dlp sudah terbaru (Versi {version})",
"could_not_determine": "Tidak dapat menentukan versi.",
"error_comparing": "Kesalahan membandingkan versi: {error}",
"update_available_failed": "Pembaruan tersedia! (Perbandingan gagal)\nSaat ini: {current}\nTerbaru: {latest}",
"updating": "Memperbarui...",
"initializing": "🚀 Menginisialisasi proses pembaruan...",
"checking_current": "🔍 Memeriksa instalasi saat ini...",
"found_at": "📍 yt-dlp ditemukan di: {path}",
"error_getting_path": "❌ Kesalahan mendapatkan jalur yt-dlp: {error}",
"updating_binary": "📦 Memperbarui binary yt-dlp yang dikelola aplikasi...",
"update_failed": "❌ Pembaruan yt-dlp gagal. Silakan coba lagi atau periksa koneksi internet Anda.",
"binary_updated": "✅ Binary berhasil diperbarui!",
"update_failed_stderr": "❌ Pembaruan yt-dlp gagal: {error}",
"update_timeout": "❌ Timeout pembaruan yt-dlp.",
"unexpected_error": "❌ Kesalahan tak terduga selama pembaruan: {error}",
"already_up_to_date": "✅ yt-dlp sudah terbaru!",
"update_success": "✅ yt-dlp telah berhasil diperbarui!",
"already_latest": "yt-dlp sudah terbaru (versi {version})",
"network_error": "❌ Kesalahan jaringan selama pembaruan: {error}",
"general_error": "❌ Pembaruan gagal: {error}",
"update_in_progress_title": "Pembaruan Sedang Berlangsung",
"update_in_progress_message": "yt-dlp sedang diperbarui. Mohon tunggu sebentar."
},
"about": {
"title": "Tentang YTSage",
"version": "Versi {version}",
"description": "Pengunduh YouTube modern dengan antarmuka PySide6 yang bersih.",
"author": "Oleh: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Terdeteksi",
"missing": "✗ Hilang",
"not_available": "Tidak tersedia"
},
"time_range": {
"title": "Potong video",
"time_range_group": "Rentang waktu",
"start_time": "Waktu mulai (HH:MM:SS)",
"end_time": "Waktu selesai (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Waktu mulai (HH:MM:SS)",
"end_time_placeholder": "Waktu selesai (HH:MM:SS)",
"force_keyframes": "Paksa keyframe pada titik potong",
"help_text": "Atur waktu mulai dan selesai untuk memotong video.\nBiarkan kosong untuk mengunduh video penuh.",
"invalid_format": "Format waktu tidak valid. Gunakan format HH:MM:SS.",
"start_after_end": "Waktu mulai tidak boleh setelah waktu selesai."
},
"settings": {
"title": "Pengaturan unduhan",
"download_path": "Jalur unduhan",
"browse": "Jelajahi...",
"speed_limit": "Batas kecepatan",
"speed_limit_placeholder": "Tidak ada",
"generic_mode": "Mode generik",
"enable_generic_mode": "Aktifkan mode generik (mendukung situs non-YouTube)",
"generic_mode_help": "Memungkinkan pengunduhan dari Dailymotion, CBC Gem, dan situs lain yang didukung oleh yt-dlp.",
"auto_update_ytdlp": "Pembaruan otomatis yt-dlp",
"enable_auto_updates": "Aktifkan pembaruan otomatis yt-dlp",
"update_frequency": "Frekuensi pembaruan:",
"check_startup": "Periksa setiap startup (minimal 1 jam antara pemeriksaan)",
"check_daily": "Periksa harian",
"check_weekly": "Periksa mingguan",
"check_updates_now": "Perbarui yt-dlp",
"update_check_title": "Pemeriksaan pembaruan",
"could_not_determine_version": "Tidak dapat menentukan versi yt-dlp saat ini.",
"update_available_dialog": "Pembaruan tersedia!\n\nSaat ini: {current}\nTerbaru: {latest}\n\nKlik OK untuk melanjutkan pembaruan.",
"up_to_date_dialog": "yt-dlp sudah terbaru!\n\nVersi saat ini: {version}",
"error_checking_updates": "Kesalahan memeriksa pembaruan: {error}",
"settings_saved_title": "Pengaturan disimpan",
"settings_saved_message": "Pengaturan pembaruan otomatis berhasil disimpan!",
"error_title": "Kesalahan",
"failed_save_settings": "Gagal menyimpan pengaturan pembaruan otomatis.",
"error_saving_settings": "Kesalahan menyimpan pengaturan pembaruan otomatis: {error}",
"ytdlp_channel": "Saluran Rilis yt-dlp",
"ytdlp_channel_stable": "Stabil (Rilis yang diuji)",
"ytdlp_channel_nightly": "Nightly (Pembaruan harian, direkomendasikan oleh yt-dlp)",
"ytdlp_channel_description": "Pilih antara rilis stabil dan nightly. Build nightly diperbarui setiap hari dengan perbaikan dan fitur terbaru. Anda dapat berganti saluran kapan saja.",
"ytdlp_switching_channel": "Beralih ke saluran {channel}...",
"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_app_updates": "Periksa pembaruan YTSage saat startup",
"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.",
"current_status": "Status saat ini",
"current_version_label": "Versi yt-dlp saat ini: Memeriksa...",
"last_check_label": "Pemeriksaan pembaruan terakhir: Tidak pernah",
"next_check_label": "Pemeriksaan berikutnya: Berdasarkan pengaturan",
"manual_check_button": "🔍 Periksa pembaruan sekarang",
"update_frequency_group": "Frekuensi pembaruan",
"save_settings": "Simpan pengaturan",
"settings_saved_successfully": "✅ Pengaturan berhasil disimpan!",
"error_saving": "❌ Kesalahan menyimpan pengaturan: {error}",
"output_format_settings": "Pengaturan Format Output",
"force_output_format": "Paksa format output saat menggabungkan",
"preferred_format": "Format yang disukai:",
"force_format_help": "Jika diaktifkan, video yang digabungkan akan dikonversi ke format pilihan Anda. Nonaktifkan untuk membiarkan yt-dlp memutuskan secara otomatis.",
"format_mp4": "MP4 (Paling kompatibel)",
"format_webm": "WebM (Modern, terbuka)",
"format_mkv": "MKV (Kaya fitur)",
"audio_format_settings": "Pengaturan Format Audio",
"force_audio_format": "Paksa format audio untuk unduhan khusus audio",
"audio_normalization": "Normalisasi Audio (EBU R128)",
"audio_normalization_help": "Jika diaktifkan, trek audio akan dinormalisasi. Catatan: Ini memerlukan pengkodean ulang, jadi format audio tertentu (seperti MP3 atau M4A) harus dipaksakan.",
"preferred_audio_format": "Format audio pilihan:",
"force_audio_format_help": "Saat diaktifkan, unduhan khusus audio akan dikonversi ke format pilihan Anda. Ini hanya berlaku saat mengunduh format audio.",
"audio_format_best": "Terbaik (Tanpa konversi)",
"audio_format_aac": "AAC (Bagus untuk editing)",
"audio_format_mp3": "MP3 (Universal)",
"audio_format_flac": "FLAC (Lossless)",
"audio_format_wav": "WAV (Tidak terkompresi)",
"audio_format_opus": "Opus (Efisien)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "Umum",
"tab_format": "Format",
"tab_file": "Berkas",
"concurrent_fragments": "Koneksi Simultan",
"concurrent_fragments_help": "Jumlah koneksi per unduhan. Nilai yang lebih tinggi melewati pembatasan tetapi dapat menyebabkan pemblokiran sementara jika diatur terlalu tinggi. Default: 1.",
"defaults_settings": "Pengaturan Pilihan Default",
"default_video_quality": "Resolusi Video Default (Tinggi):",
"default_subtitle_language": "Bahasa Subtitel Default:",
"defaults_help": "Atur tinggi video dan bahasa subtitel pilihan Anda (pisahkan dengan koma). Mereka akan dipilih otomatis jika tersedia."
},
"main_ui": {
"url_placeholder": "Masukkan URL video atau playlist YouTube",
"url_placeholder_generic": "Masukkan URL video atau playlist dari situs apa pun yang didukung",
"merge_subtitles": "Gabungkan subtitle",
"save_thumbnail": "Simpan thumbnail",
"save_description": "Simpan deskripsi",
"embed_chapters": "Sematkan bab",
"subtitles_selected": "{count} dipilih",
"all_selected": "Semua dipilih",
"select_videos_all": "Pilih video... (Semua dipilih)",
"please_enter_url": "Silakan masukkan URL terlebih dahulu",
"error_title": "Kesalahan",
"cookie_file_selected_title": "File cookie dipilih",
"cookie_file_selected_message": "File cookie dipilih: {path}",
"browser_cookies_selected_title": "Cookies browser dipilih",
"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": "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": "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",
"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",
"sponsor_desc": "Iklan berbayar, rekomendasi berbayar dan iklan langsung",
"selfpromo": "Promosi diri tidak berbayar",
"selfpromo_desc": "Iklan tidak berbayar untuk konten kreator sendiri",
"interaction": "Pengingat interaksi",
"interaction_desc": "Pemirsa diminta untuk menyukai, berlangganan atau mengikuti media sosial",
"intro": "Intro",
"intro_desc": "Intro video yang dapat dilewati",
"outro": "Outro/Kartu akhir",
"outro_desc": "Kredit atau saat video berakhir",
"preview": "Pratinjau/Ringkasan",
"preview_desc": "Ringkasan singkat video sebelumnya atau pratinjau konten mendatang",
"music_offtopic": "Bagian non-musik",
"music_offtopic_desc": "Hanya untuk video musik. Menandai bagian non-musik",
"filler": "Tangensial pengisi",
"filler_desc": "Adegan tangensial yang ditambahkan hanya sebagai pengisi atau untuk humor"
},
"video_info": {
"channel": "Saluran",
"views": "👁 Tayangan",
"likes": "👍 Suka",
"upload_date": "📅 Tanggal upload",
"duration": "⏱ Durasi",
"unknown_channel": "Saluran tidak diketahui",
"unknown_date": "Tanggal tidak diketahui",
"unknown_title": "Judul tidak diketahui"
},
"command": {
"running": "Berjalan...",
"run_command": "Jalankan perintah"
},
"selection": {
"none_selected": "0 dipilih",
"one_selected": "1 kategori dipilih",
"count_selected": "{count} dipilih"
},
"status": {
"ready": "Siap",
"file_exists": "⚠️ File sudah ada",
"video_file_exists": "⚠️ File video sudah ada",
"audio_file_exists": "⚠️ File audio sudah ada",
"subtitle_file_exists": "⚠️ File subtitle sudah ada",
"cancelling": "Membatalkan unduhan...",
"thumbnail_saved": "✅ Thumbnail disimpan: {filename}",
"thumbnail_error": "❌ Kesalahan thumbnail: {error}",
"thumbnail_no_image": "Tidak ada thumbnail yang tersedia untuk disimpan"
},
"errors": {
"playlist_no_videos": "Kesalahan: Playlist tidak berisi video yang valid.",
"playlist_no_url": "Kesalahan: Tidak dapat mendapatkan URL untuk video playlist pertama.",
"ytdlp_not_found": "Kesalahan: Executable yt-dlp tidak ditemukan. Silakan instal yt-dlp terlebih dahulu.",
"ytdlp_not_found_path": "Kesalahan: Executable yt-dlp tidak ditemukan. Ini bisa karena instalasi yang tidak tepat atau masalah PATH.",
"no_data_returned": "Kesalahan: Tidak ada data yang dikembalikan dari yt-dlp",
"no_format_info": "Kesalahan: Tidak ada informasi format yang tersedia.",
"analysis_timeout": "Kesalahan: Analisis timeout. Silakan coba lagi.",
"invalid_speed_limit": "❌ Kesalahan: Nilai batas kecepatan tidak valid yang diatur dalam pengaturan.",
"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}",
"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}",
"private_video": "Video ini mungkin bersifat pribadi. Silakan gunakan cookie dari opsi kustom."
},
"update_dialog": {
"title": "Pembaruan Tersedia",
"new_version_available": "Versi baru YTSage tersedia!",
"current_version_label": "Versi saat ini:",
"latest_version_label": "Versi terbaru:",
"changelog": "Catatan perubahan",
"download_update": "Unduh Pembaruan",
"remind_later": "Ingatkan Nanti"
},
"playlist": {
"unknown": "Playlist Tidak Dikenal",
"total_videos": "Total Video: {count}",
"display_format": "Playlist: {title} | {count} video",
"select_videos_title": "Pilih Video Playlist",
"save_as": "Simpan Playlist Sebagai",
"save_success_title": "Berhasil",
"saved_successfully": "Playlist berhasil disimpan.",
"save_error_title": "Gagal Menyimpan",
"no_videos_to_save": "Tidak ada entri playlist yang dikumpulkan!",
"save_error_msg": "Gagal menyimpan playlist."
},
"subtitle_selection": {
"count_selected": "{count} dipilih"
},
"file_exists_dialog": {
"title": "File Sudah Ada",
"message": "File sudah ada:\n{filename}",
"info": "Video ini sudah pernah diunduh."
},
"auto_update": {
"last_check_never": "Pemeriksaan terakhir: Tidak pernah",
"last_check": "Pemeriksaan terakhir: {time}",
"next_check_disabled": "Pemeriksaan berikutnya: Dinonaktifkan",
"next_check_startup": "Pemeriksaan berikutnya: Saat startup",
"next_check_overdue": "Pemeriksaan berikutnya: Sekarang (terlambat)",
"next_check": "Pemeriksaan berikutnya: {time}",
"next_check_error": "Pemeriksaan berikutnya: Kesalahan perhitungan",
"checking": "🔄 Memeriksa...",
"check_now": "🔍 Periksa pembaruan sekarang",
"current_version": "Versi yt-dlp saat ini: {version}"
},
"url_validation": {
"empty_url": "URL tidak boleh kosong",
"invalid_format": "Format URL tidak valid",
"invalid_scheme": "URL harus dimulai dengan http:// atau https://",
"missing_domain": "URL tidak valid: nama domain hilang",
"unsupported_platform": "YTSage hanya mendukung URL YouTube dan YouTube Music.\nDomain '{domain}' tidak didukung.",
"invalid_youtu_be": "URL youtu.be tidak valid: ID video hilang"
},
"ytdlp_errors": {
"private_video": "Ini adalah video privat. Anda dapat mengunduhnya dengan masuk ke akun Anda menggunakan cookie.\nBuka 'Opsi Kustom' → 'Masuk dengan Cookie' → 'Ekstrak cookie dari browser' untuk autentikasi.",
"age_restricted": "Video ini dibatasi usia. Anda perlu masuk untuk mengaksesnya.\nGunakan 'Opsi Kustom' → 'Masuk dengan Cookie' untuk autentikasi dengan akun Anda.",
"geo_blocked": "Video ini tidak tersedia di wilayah Anda (diblokir geografis).\nAnda mungkin perlu menggunakan VPN atau video mungkin dibatasi di negara Anda.",
"video_unavailable": "Video ini telah dihapus atau tidak lagi tersedia.\nVideo mungkin telah dihapus oleh pengunggah atau dihapus karena pelanggaran kebijakan.",
"live_stream": "Ini adalah siaran langsung yang tidak dapat diunduh saat aktif.\nTunggu hingga siaran berakhir, lalu coba unduh versi arsip.",
"playlist_error": "Tidak dapat mengakses playlist ini. Mungkin privat, dihapus, atau kosong.\nPeriksa apakah playlist ada dan dapat diakses publik.",
"network_error": "Kesalahan koneksi jaringan. Silakan periksa koneksi internet Anda dan coba lagi.\nJika masalah berlanjut, server video mungkin tidak tersedia sementara.",
"invalid_url": "URL tidak valid atau tidak didukung. Silakan periksa tautan dan coba lagi.\nPastikan Anda menggunakan URL YouTube, Vimeo, atau platform didukung lainnya yang valid.",
"premium_content": "Konten ini memerlukan YouTube Premium atau keanggotaan channel.\nAnda perlu masuk dengan akun yang memiliki akses ke konten ini.",
"copyright_blocked": "Video ini diblokir karena klaim hak cipta.\nPemilik konten telah membatasi akses ke video ini.",
"extraction_failed": "Gagal mengekstrak informasi video. Ini mungkin masalah sementara.\nSilakan coba lagi dalam beberapa menit, atau periksa apakah tautan video benar.",
"generic_error": "Tidak dapat mengekstrak informasi video. Silakan periksa tautan Anda.\nDetail teknis: {error}"
},
"history": {
"title": "Riwayat Unduhan",
"clear_all": "Hapus Semua",
"clear_confirm_title": "Hapus Riwayat?",
"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",
"remove": "Hapus",
"file_not_found": "File tidak ditemukan",
"file_not_found_message": "File dipindahkan atau dihapus:\n{path}",
"downloaded_on": "Diunduh: {date}",
"file_size": "Ukuran: {size}",
"audio_download": "Audio",
"video_download": "Video",
"remove_confirm_title": "Hapus?",
"remove_confirm_message": "Hapus dari riwayat?\n\n{title}",
"item_removed": "Dihapus",
"history_cleared": "Riwayat dihapus",
"entries_count": "{count} unduhan",
"one_entry": "1 unduhan",
"redownload_confirm_title": "Unduh Lagi?",
"redownload_confirm_message": "Unduh lagi?\n\n{title}",
"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",
"current_version": "Versi Saat Ini:",
"latest_version": "Versi Terbaru:",
"status_up_to_date": "✓ Terbaru",
"status_update_available": "⚠ Pembaruan tersedia",
"status_not_installed": "✗ Tidak terinstal",
"status_idle": "Klik 'Periksa Versi' untuk memulai",
"status_checking": "🔄 Memeriksa versi...",
"check_updates": "Periksa Versi",
"check_failed": "Gagal memeriksa versi. Periksa koneksi internet Anda.",
"description": "Periksa versi FFmpeg Anda dan bandingkan dengan versi terbaru yang tersedia.",
"guide_info": " Untuk menginstal atau memperbarui FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>klik di sini untuk melihat panduan instalasi lengkap kami</a>."
},
"deno": {
"setup_required": "Pengaturan Deno Diperlukan",
"setup_description": "YTSage memerlukan Deno untuk menjalankan fitur tertentu.<br><br>Deno tidak ditemukan di direktori lokal aplikasi. YTSage perlu mengatur Deno untuk sistem {os_name} Anda.<br><br>Klik 'Atur Deno' untuk mengunduh dan menginstal secara otomatis.",
"setup_button": "Atur Deno",
"downloading": "Mengunduh Deno...",
"extracting": "Mengekstrak Deno...",
"verifying": "Memverifikasi Deno...",
"success": "Deno berhasil diinstal!",
"download_failed": "Unduhan Gagal",
"download_error": "Gagal mengunduh Deno: {error}",
"verification_failed": "Verifikasi SHA256 gagal. File yang diunduh mungkin rusak atau dimanipulasi.",
"setup_failed": "Gagal mengatur Deno. Beberapa fitur mungkin tidak berfungsi dengan benar.",
"setup_error": "Kesalahan Pengaturan"
},
"deno_updater": {
"title": "Pemeriksa & Pembaruan Versi Deno",
"description": "Periksa versi Deno Anda dan perbarui ke rilis terbaru.",
"current_version": "Versi Saat Ini:",
"latest_version": "Versi Terbaru:",
"status_idle": "Klik 'Periksa Pembaruan' untuk memulai",
"status_checking": "🔄 Memeriksa pembaruan...",
"status_up_to_date": "✓ Terbaru",
"status_update_available": "⚠ Pembaruan tersedia",
"status_not_installed": "✗ Tidak terinstal",
"check_updates": "Periksa Pembaruan",
"update_now": "Perbarui Deno",
"updating": "🔄 Memperbarui Deno...",
"update_success": "✅ Deno telah berhasil diperbarui!",
"update_failed": "❌ Pembaruan gagal: {error}",
"check_failed": "Gagal memeriksa pembaruan. Harap periksa koneksi internet Anda."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Italiano (Italian)",
"select_language": "Seleziona lingua:",
"current_language": "Lingua attuale: {language}",
"restart_required": "Il cambio di lingua avrà effetto dopo il riavvio dell'applicazione.",
"english": "Inglese",
"spanish": "Spagnolo",
"portuguese": "Portoghese",
"russian": "Russo",
"chinese": "Cinese",
"german": "Tedesco",
"french": "Francese",
"hindi": "Hindi",
"indonesian": "Indonesiano",
"turkish": "Turco",
"polish": "Polacco",
"italian": "Italiano",
"arabic": "Arabo",
"japanese": "Giapponese",
"help_text": "Seleziona la lingua preferita per l'interfaccia.",
"restart_notice": "Il cambio di lingua avrà effetto dopo il riavvio dell'applicazione."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Pronto"
},
"formats": {
"show_formats": "Mostra formati:",
"video_format": "Formato video:",
"audio_format": "Formato audio:",
"no_formats": "Nessun formato disponibile",
"loading": "Caricamento formati...",
"select": "Seleziona",
"quality": "Qualità",
"extension": "Estensione",
"resolution": "Risoluzione",
"file_size": "Dimensione file",
"codec": "Codec",
"audio": "Audio",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "L'audio verrà unito",
"has_audio": "✓ Contiene audio",
"audio_only": "Solo audio",
"best_4k": "Migliore (4K)",
"best_2k": "Migliore (2K)",
"high_1080p": "Alta (1080p)",
"high_720p": "Alta (720p)",
"medium_480p": "Media (480p)",
"low_quality": "Bassa qualità",
"best_audio": "Miglior audio",
"high_audio": "Audio alto",
"medium_audio": "Audio medio",
"low_audio": "Audio basso",
"audio_only_resolution": "Solo audio"
},
"buttons": {
"reset": "Reimposta",
"download": "Scarica",
"pause": "Pausa",
"resume": "Riprendi",
"cancel": "Annulla",
"browse": "Sfoglia",
"clear": "Cancella",
"ok": "OK",
"apply": "Applica",
"close": "Chiudi",
"run_command": "Esegui comando",
"custom_command_help": "Aiuto",
"about": "Informazioni",
"analyze": "Analizza",
"paste_url": "Incolla URL",
"select_videos": "Seleziona video...",
"video": "Video",
"audio_only": "Solo audio",
"custom_options": "Opzioni personalizzate",
"trim_video": "Taglia video",
"download_settings": "Impostazioni download",
"update": "Aggiorna yt-dlp",
"select_defaults": "Seleziona predefiniti",
"select_all": "Seleziona tutto",
"deselect_all": "Deseleziona tutto",
"open_folder": "Apri posizione cartella",
"history": "Cronologia",
"save_playlist": "Salva playlist come"
},
"dialogs": {
"custom_options": "Opzioni personalizzate",
"settings": "Impostazioni",
"select_folder": "Seleziona cartella di download",
"sponsorblock_categories": "Categorie SponsorBlock",
"sponsorblock_description": "Seleziona i tipi di segmenti video da rimuovere automaticamente durante il download.\nSponsorBlock utilizza dati inviati dalla comunità per identificare questi segmenti.",
"select_subtitles": "Seleziona sottotitoli",
"filter_languages_placeholder": "Filtra lingue (es: it, en)...",
"no_subtitles_available": "Nessun sottotitolo disponibile",
"matching": "corrispondenti",
"ytdlp_log_title": "Registro yt-dlp",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Accedi con i cookie",
"custom_command": "Comando personalizzato",
"proxy": "Proxy",
"language": "Lingua",
"updater": "Aggiornamento"
},
"cookies": {
"help_text": "Seleziona un metodo per fornire i cookie per l'autenticazione.\nCiò consente di scaricare video privati e file audio di alta qualità.",
"cookie_source": "Fonte cookie",
"use_cookie_file": "Usa file cookie",
"extract_from_browser": "Estrai dal browser",
"recommended": "Consigliato",
"cookie_file": "File cookie",
"cookie_file_placeholder": "Percorso al file cookies.txt...",
"browser_selection": "Selezione browser",
"browser_help": "Seleziona il browser da cui estrarre i cookie:",
"browser_label": "Browser:",
"profile_label": "Profilo:",
"profile_placeholder": "Predefinito",
"browser_extract_message": "I cookie del browser verranno estratti dopo l'applicazione",
"file_selected_message": "File cookie selezionato - Clicca OK per applicare",
"select_file_title": "Seleziona file cookie",
"file_filter": "File cookie (*.txt *.lwp)",
"file_selected_title": "File Cookie Applicato",
"file_applied_message": "File cookie applicato: {path}",
"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",
"active_browser": "✓ Attivi: cookie del browser ({browser})",
"active_file": "✓ Attivo: file cookie ({file})",
"none_active": "○ Nessun cookie attivo",
"remember_settings": "Ricorda le impostazioni dei cookie al prossimo avvio"
},
"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.",
"input_label": "Argomenti yt-dlp:",
"input_placeholder": "Inserisci gli argomenti yt-dlp qui...\n\nEsempio: --extract-audio --audio-format mp3",
"output_label": "Output comando:",
"output_placeholder": "L'output del comando apparirà qui...",
"command_placeholder": "Inserisci comando yt-dlp personalizzato...",
"command_help": "Placeholder disponibili:\n{url} - URL del video\n{output} - Cartella di output\n\nEsempio: --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Proxy principale",
"main_proxy_help": "Server proxy principale per tutti i download. Supporta protocolli HTTP/HTTPS e SOCKS5.",
"proxy_url_label": "URL Proxy:",
"proxy_url_placeholder": "http://proxy:porta o socks5://proxy:porta",
"proxy_examples": "Esempi:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Proxy geo-bypass",
"geo_proxy_help": "Proxy aggiuntivo specificamente per aggirare le restrizioni geografiche.",
"geo_proxy_url_label": "URL proxy geo-bypass:",
"geo_proxy_url_placeholder": "http://geo-proxy:porta",
"clear_main_proxy": "Cancella proxy principale",
"clear_geo_proxy": "Cancella proxy geo",
"proxy_url": "URL Proxy",
"proxy_placeholder": "http://proxy:porta o socks5://proxy:porta",
"geo_bypass": "Proxy geo-bypass (per restrizioni geografiche)",
"geo_bypass_placeholder": "http://proxy:porta per aggirare il geo-blocking",
"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",
"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...",
"starting": "🚀 Avvio download...",
"fetching_info": "🔍 Recupero informazioni video...",
"preparing_streams": "🎯 Preparazione stream video...",
"downloading_audio": "⏬ Download audio...",
"downloading_video": "⏬ Download video...",
"downloading_subtitle": "⏬ Download sottotitoli...",
"downloading": "⏬ Download...",
"completed": "✅ Download completato!",
"video_completed": "✅ Download video completato!",
"audio_completed": "✅ Download audio completato!",
"subtitle_completed": "✅ Download sottotitoli completato!",
"completed_cleaning": "✅ Download completato! Pulizia in corso...",
"cancelled": "Download annullato",
"processing_playlist": "📋 Elaborazione dati playlist...",
"paused": "Download in pausa",
"resumed": "Download ripreso",
"merging_formats": "✨ Post-elaborazione: Unione formati...",
"removing_sponsor_segments": "✨ Post-elaborazione: Rimozione segmenti sponsorizzati...",
"speed": "Velocità",
"eta": "Tempo rimanente",
"please_enter_url": "Inserisci prima un URL.",
"please_set_path": "Imposta prima un percorso di download.",
"please_enter_url_and_path": "Inserisci un URL e imposta un percorso di download.",
"please_select_format": "Seleziona prima un formato.",
"downloading_fallback": "⚡ Download in corso..."
},
"update": {
"title": "Aggiorna yt-dlp",
"checking": "Controllo aggiornamenti...",
"update_available": "Aggiornamento disponibile!\nVersione corrente: {current}\nUltima versione: {latest}",
"up_to_date": "yt-dlp è aggiornato (Versione {version})",
"could_not_determine": "Impossibile determinare la versione.",
"error_comparing": "Errore nel confronto versioni: {error}",
"update_available_failed": "Aggiornamento disponibile! (Confronto fallito)\nCorrente: {current}\nUltima: {latest}",
"updating": "Aggiornamento...",
"initializing": "🚀 Inizializzazione processo di aggiornamento...",
"checking_current": "🔍 Controllo installazione corrente...",
"found_at": "📍 yt-dlp trovato in: {path}",
"error_getting_path": "❌ Errore nel recupero del percorso yt-dlp: {error}",
"updating_binary": "📦 Aggiornamento binario yt-dlp gestito dall'app...",
"update_failed": "❌ Aggiornamento yt-dlp fallito. Riprova o controlla la connessione internet.",
"binary_updated": "✅ Binario aggiornato con successo!",
"update_failed_stderr": "❌ Aggiornamento yt-dlp fallito: {error}",
"update_timeout": "❌ Timeout aggiornamento yt-dlp.",
"unexpected_error": "❌ Errore inaspettato durante l'aggiornamento: {error}",
"already_up_to_date": "✅ yt-dlp è già aggiornato!",
"update_success": "✅ yt-dlp è stato aggiornato con successo!",
"already_latest": "yt-dlp è aggiornato (versione {version})",
"network_error": "❌ Errore di rete durante l'aggiornamento: {error}",
"general_error": "❌ Aggiornamento fallito: {error}",
"update_in_progress_title": "Aggiornamento in corso",
"update_in_progress_message": "yt-dlp si sta aggiornando. Attendere prego."
},
"about": {
"title": "Informazioni su YTSage",
"version": "Versione {version}",
"description": "Downloader YouTube moderno con interfaccia PySide6 pulita.",
"author": "Autore: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Rilevato",
"missing": "✗ Mancante",
"not_available": "Non disponibile"
},
"time_range": {
"title": "Taglia video",
"time_range_group": "Intervallo di tempo",
"start_time": "Tempo di inizio (HH:MM:SS)",
"end_time": "Tempo di fine (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Tempo di inizio (HH:MM:SS)",
"end_time_placeholder": "Tempo di fine (HH:MM:SS)",
"force_keyframes": "Forza keyframe ai punti di taglio",
"help_text": "Imposta i tempi di inizio e fine per tagliare il video.\nLascia vuoto per scaricare il video completo.",
"invalid_format": "Formato tempo non valido. Usa il formato HH:MM:SS.",
"start_after_end": "Il tempo di inizio non può essere dopo il tempo di fine."
},
"settings": {
"title": "Impostazioni download",
"download_path": "Percorso download",
"browse": "Sfoglia...",
"speed_limit": "Limite velocità",
"speed_limit_placeholder": "Nessuno",
"generic_mode": "Modalità generica",
"enable_generic_mode": "Abilita modalità generica (supporta siti non YouTube)",
"generic_mode_help": "Consente il download da Dailymotion, CBC Gem e altri siti supportati da yt-dlp.",
"auto_update_ytdlp": "Aggiornamenti automatici yt-dlp",
"enable_auto_updates": "Abilita aggiornamenti automatici yt-dlp",
"update_frequency": "Frequenza aggiornamenti:",
"check_startup": "Controlla ad ogni avvio (minimo 1 ora tra i controlli)",
"check_daily": "Controlla giornalmente",
"check_weekly": "Controlla settimanalmente",
"check_updates_now": "Aggiorna yt-dlp",
"update_check_title": "Controllo aggiornamenti",
"could_not_determine_version": "Impossibile determinare la versione corrente di yt-dlp.",
"update_available_dialog": "Aggiornamento disponibile!\n\nCorrente: {current}\nUltima: {latest}\n\nClicca su OK per procedere con l'aggiornamento.",
"up_to_date_dialog": "yt-dlp è aggiornato!\n\nVersione corrente: {version}",
"error_checking_updates": "Errore nel controllo aggiornamenti: {error}",
"settings_saved_title": "Impostazioni salvate",
"settings_saved_message": "Le impostazioni di aggiornamento automatico sono state salvate con successo!",
"error_title": "Errore",
"failed_save_settings": "Impossibile salvare le impostazioni di aggiornamento automatico.",
"error_saving_settings": "Errore nel salvataggio delle impostazioni di aggiornamento automatico: {error}",
"ytdlp_channel": "Canale di rilascio yt-dlp",
"ytdlp_channel_stable": "Stabile (Rilasci testati)",
"ytdlp_channel_nightly": "Nightly (Aggiornamenti giornalieri, consigliato da yt-dlp)",
"ytdlp_channel_description": "Scegli tra rilasci stabili e nightly. Le build nightly vengono aggiornate quotidianamente con le ultime correzioni e funzionalità. Puoi cambiare canale in qualsiasi momento.",
"ytdlp_switching_channel": "Passaggio al canale {channel}...",
"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_app_updates": "Controlla aggiornamenti YTSage all'avvio",
"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.",
"current_status": "Stato attuale",
"current_version_label": "Versione corrente yt-dlp: Controllo...",
"last_check_label": "Ultimo controllo aggiornamenti: Mai",
"next_check_label": "Prossimo controllo: Basato sulle impostazioni",
"manual_check_button": "🔍 Controlla aggiornamenti ora",
"update_frequency_group": "Frequenza aggiornamenti",
"save_settings": "Salva impostazioni",
"settings_saved_successfully": "✅ Impostazioni salvate con successo!",
"error_saving": "❌ Errore nel salvataggio impostazioni: {error}",
"output_format_settings": "Impostazioni formato output",
"force_output_format": "Forza formato output durante la fusione",
"preferred_format": "Formato preferito:",
"force_format_help": "Quando attivato, i video uniti verranno convertiti nel formato preferito. Disattiva per lasciare che yt-dlp decida automaticamente.",
"format_mp4": "MP4 (Più compatibile)",
"format_webm": "WebM (Moderno, aperto)",
"format_mkv": "MKV (Ricco di funzionalità)",
"audio_format_settings": "Impostazioni formato audio",
"force_audio_format": "Forza formato audio per download solo audio",
"audio_normalization": "Normalizzazione audio (EBU R128)",
"audio_normalization_help": "Se abilitato, le tracce audio verranno normalizzate. Nota: ciò richiede la ricodifica, quindi deve essere forzato un formato audio specifico (come MP3 o M4A).",
"preferred_audio_format": "Formato audio preferito:",
"force_audio_format_help": "Quando abilitato, i download solo audio verranno convertiti nel formato preferito. Questo si applica solo durante il download di formati audio.",
"audio_format_best": "Migliore (Nessuna conversione)",
"audio_format_aac": "AAC (Buono per l'editing)",
"audio_format_mp3": "MP3 (Universale)",
"audio_format_flac": "FLAC (Senza perdita)",
"audio_format_wav": "WAV (Non compresso)",
"audio_format_opus": "Opus (Efficiente)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "Generale",
"tab_format": "Formato",
"tab_file": "File",
"concurrent_fragments": "Connessioni simultanee",
"concurrent_fragments_help": "Numero di connessioni per download. Valori più alti aggirano il throttling ma possono causare blocchi temporanei se impostati troppo alti. Predefinito: 1.",
"defaults_settings": "Impostazioni di selezione predefinite",
"default_video_quality": "Risoluzione video predefinita (altezza):",
"default_subtitle_language": "Lingua/e dei sottotitoli predefinita/e:",
"defaults_help": "Imposta l'altezza del video preferita e le lingue dei sottotitoli (separate da virgole). Verranno selezionate automaticamente se disponibili."
},
"main_ui": {
"url_placeholder": "Inserisci URL video YouTube o playlist",
"url_placeholder_generic": "Inserisci l'URL di un video o di una playlist da qualsiasi sito supportato",
"merge_subtitles": "Unisci sottotitoli",
"save_thumbnail": "Salva miniatura",
"save_description": "Salva descrizione",
"embed_chapters": "Incorpora capitoli",
"subtitles_selected": "{count} selezionati",
"all_selected": "Tutti selezionati",
"select_videos_all": "Seleziona video... (Tutti selezionati)",
"please_enter_url": "Inserisci prima un URL",
"error_title": "Errore",
"cookie_file_selected_title": "File cookie selezionato",
"cookie_file_selected_message": "File cookie selezionato: {path}",
"browser_cookies_selected_title": "Cookie browser selezionati",
"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": "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": "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",
"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",
"sponsor_desc": "Pubblicità a pagamento, raccomandazioni a pagamento e pubblicità dirette",
"selfpromo": "Non remunerata/Autopromozione",
"selfpromo_desc": "Promozione non remunerata dei propri contenuti del creatore",
"interaction": "Promemoria interazione",
"interaction_desc": "Gli spettatori vengono invitati a mettere mi piace, iscriversi o seguire sui social media",
"intro": "Intro",
"intro_desc": "Introduzione al video che può essere saltata",
"outro": "Outro/Schede finali",
"outro_desc": "Titoli di coda o quando il video finisce",
"preview": "Anteprima/Ricapitolazione",
"preview_desc": "Breve ricapitolazione di video precedenti o anteprima di contenuti futuri",
"music_offtopic": "Sezione non musicale",
"music_offtopic_desc": "Solo per video musicali. Indica sezioni non musicali",
"filler": "Riempitivo tangenziale",
"filler_desc": "Scene tangenziali aggiunte solo come riempitivo o per umorismo"
},
"video_info": {
"channel": "Canale",
"views": "👁 Visualizzazioni",
"likes": "👍 Mi piace",
"upload_date": "📅 Data di caricamento",
"duration": "⏱ Durata",
"unknown_channel": "Canale sconosciuto",
"unknown_date": "Data sconosciuta",
"unknown_title": "Titolo sconosciuto"
},
"command": {
"running": "Esecuzione...",
"run_command": "Esegui comando"
},
"selection": {
"none_selected": "0 selezionati",
"one_selected": "1 categoria selezionata",
"count_selected": "{count} selezionati"
},
"status": {
"ready": "Pronto",
"file_exists": "⚠️ File già esistente",
"video_file_exists": "⚠️ File video già esistente",
"audio_file_exists": "⚠️ File audio già esistente",
"subtitle_file_exists": "⚠️ File sottotitoli già esistente",
"cancelling": "Annullamento download...",
"thumbnail_saved": "✅ Miniatura salvata: {filename}",
"thumbnail_error": "❌ Errore miniatura: {error}",
"thumbnail_no_image": "Nessuna miniatura disponibile da salvare"
},
"errors": {
"playlist_no_videos": "Errore: La playlist non contiene video validi.",
"playlist_no_url": "Errore: Impossibile ottenere l'URL del primo video della playlist.",
"ytdlp_not_found": "Errore: Eseguibile yt-dlp non trovato. Installare prima yt-dlp.",
"ytdlp_not_found_path": "Errore: Eseguibile yt-dlp non trovato. Ciò potrebbe essere dovuto a un'installazione errata o a un problema di PATH.",
"no_data_returned": "Errore: Nessun dato restituito da yt-dlp",
"no_format_info": "Errore: Nessuna informazione formato disponibile.",
"analysis_timeout": "Errore: Timeout analisi. Riprovare.",
"invalid_speed_limit": "❌ Errore: Valore limite velocità non valido impostato nelle impostazioni.",
"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}",
"download_failed_return_code_conflict": "Download non riuscito con codice di uscita {return_code}. Potrebbe essere dovuto a un conflitto tra più installazioni di yt-dlp. Prova a disinstallare eventuali yt-dlp installati a livello di sistema (es. snap o apt) e riavvia lapp.",
"download_failed_return_code": "Download non riuscito con codice di uscita {return_code}",
"direct_command_error": "Errore nel comando diretto: {error}",
"private_video": "Questo video potrebbe essere privato. Si prega di utilizzare i cookie dalle opzioni personalizzate."
},
"update_dialog": {
"title": "Aggiornamento disponibile",
"new_version_available": "È disponibile una nuova versione di YTSage!",
"current_version_label": "Versione corrente:",
"latest_version_label": "Ultima versione:",
"changelog": "Registro modifiche",
"download_update": "Scarica aggiornamento",
"remind_later": "Ricordamelo dopo"
},
"playlist": {
"unknown": "Playlist sconosciuta",
"total_videos": "Video totali: {count}",
"display_format": "Playlist: {title} | {count} video",
"select_videos_title": "Seleziona Video Playlist",
"save_as": "Salva playlist come",
"save_success_title": "Successo",
"saved_successfully": "Playlist salvata con successo.",
"save_error_title": "Errore di salvataggio",
"no_videos_to_save": "Nessuna voce della playlist raccolta!",
"save_error_msg": "Impossibile salvare la playlist."
},
"subtitle_selection": {
"count_selected": "{count} selezionati"
},
"file_exists_dialog": {
"title": "Il file esiste già",
"message": "Il file esiste già:\n{filename}",
"info": "Questo video è già stato scaricato."
},
"auto_update": {
"last_check_never": "Ultimo controllo aggiornamenti: Mai",
"last_check": "Ultimo controllo aggiornamenti: {time}",
"next_check_disabled": "Prossimo controllo: Disabilitato",
"next_check_startup": "Prossimo controllo: All'avvio",
"next_check_overdue": "Prossimo controllo: Ora (in ritardo)",
"next_check": "Prossimo controllo: {time}",
"next_check_error": "Prossimo controllo: Errore di calcolo",
"checking": "🔄 Controllo...",
"check_now": "🔍 Controlla aggiornamenti ora",
"current_version": "Versione corrente di yt-dlp: {version}"
},
"url_validation": {
"empty_url": "L'URL non può essere vuoto",
"invalid_format": "Formato URL non valido",
"invalid_scheme": "L'URL deve iniziare con http:// o https://",
"missing_domain": "URL non valido: nome di dominio mancante",
"unsupported_platform": "YTSage supporta solo URL di YouTube e YouTube Music.\nIl dominio '{domain}' non è supportato.",
"invalid_youtu_be": "URL youtu.be non valido: ID video mancante"
},
"ytdlp_errors": {
"private_video": "Questo è un video privato. Puoi scaricarlo accedendo al tuo account utilizzando i cookie.\nVai su 'Opzioni Personalizzate' → 'Accedi con Cookie' → 'Estrai cookie dal browser' per autenticarti.",
"age_restricted": "Questo video è soggetto a restrizioni di età. Devi effettuare l'accesso per accedervi.\nUtilizza 'Opzioni Personalizzate' → 'Accedi con Cookie' per autenticarti con il tuo account.",
"geo_blocked": "Questo video non è disponibile nella tua regione (geo-bloccato).\nPotresti aver bisogno di utilizzare una VPN o il video potrebbe essere limitato nel tuo paese.",
"video_unavailable": "Questo video è stato rimosso o non è più disponibile.\nIl video potrebbe essere stato eliminato dall'uploader o rimosso a causa di violazioni delle politiche.",
"live_stream": "Questo è uno stream live che non può essere scaricato mentre è attivo.\nAttendi la fine dello stream, quindi prova a scaricare la versione archiviata.",
"playlist_error": "Impossibile accedere a questa playlist. Potrebbe essere privata, eliminata o vuota.\nVerifica se la playlist esiste ed è accessibile pubblicamente.",
"network_error": "Errore di connessione di rete. Controlla la tua connessione Internet e riprova.\nSe il problema persiste, il server video potrebbe essere temporaneamente non disponibile.",
"invalid_url": "URL non valido o non supportato. Controlla il link e riprova.\nAssicurati di utilizzare un URL YouTube, Vimeo o altra piattaforma supportata valido.",
"premium_content": "Questo contenuto richiede YouTube Premium o l'iscrizione al canale.\nDevi accedere con un account che ha accesso a questo contenuto.",
"copyright_blocked": "Questo video è bloccato a causa di rivendicazioni di copyright.\nIl proprietario del contenuto ha limitato l'accesso a questo video.",
"extraction_failed": "Impossibile estrarre le informazioni del video. Questo potrebbe essere un problema temporaneo.\nRiprova tra qualche minuto o verifica se il link del video è corretto.",
"generic_error": "Impossibile estrarre le informazioni del video. Controlla il tuo link.\nDettagli tecnici: {error}"
},
"history": {
"title": "Cronologia Download",
"clear_all": "Cancella Tutto",
"clear_confirm_title": "Cancellare Cronologia?",
"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",
"remove": "Rimuovi",
"file_not_found": "File non trovato",
"file_not_found_message": "Il file è stato spostato o eliminato:\n{path}",
"downloaded_on": "Scaricato: {date}",
"file_size": "Dimensione: {size}",
"audio_download": "Audio",
"video_download": "Video",
"remove_confirm_title": "Rimuovere?",
"remove_confirm_message": "Rimuovere dalla cronologia?\n\n{title}",
"item_removed": "Rimosso",
"history_cleared": "Cronologia cancellata",
"entries_count": "{count} download",
"one_entry": "1 download",
"redownload_confirm_title": "Scaricare di Nuovo?",
"redownload_confirm_message": "Scaricare di nuovo?\n\n{title}",
"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",
"current_version": "Versione corrente:",
"latest_version": "Ultima versione:",
"status_up_to_date": "✓ Aggiornato",
"status_update_available": "⚠ Aggiornamento disponibile",
"status_not_installed": "✗ Non installato",
"status_idle": "Clicca 'Controlla Versione' per iniziare",
"status_checking": "🔄 Verifica versione...",
"check_updates": "Controlla Versione",
"check_failed": "Verifica versione non riuscita. Controlla la connessione Internet.",
"description": "Controlla la tua versione di FFmpeg e confrontala con l'ultima versione disponibile.",
"guide_info": " Per installare o aggiornare FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>clicca qui per visualizzare la nostra guida completa all'installazione</a>."
},
"deno": {
"setup_required": "Configurazione Deno richiesta",
"setup_description": "YTSage richiede Deno per eseguire alcune funzionalità.<br><br>Deno non è stato trovato nella directory locale dell'app. YTSage deve configurare Deno per il tuo sistema {os_name}.<br><br>Fai clic su 'Configura Deno' per scaricare e installare automaticamente.",
"setup_button": "Configura Deno",
"downloading": "Download di Deno in corso...",
"extracting": "Estrazione di Deno in corso...",
"verifying": "Verifica di Deno in corso...",
"success": "Deno è stato installato con successo!",
"download_failed": "Download fallito",
"download_error": "Impossibile scaricare Deno: {error}",
"verification_failed": "Verifica SHA256 fallita. Il file scaricato potrebbe essere danneggiato o manomesso.",
"setup_failed": "Impossibile configurare Deno. Alcune funzionalità potrebbero non funzionare correttamente.",
"setup_error": "Errore di configurazione"
},
"deno_updater": {
"title": "Verificatore e Aggiornatore Versione Deno",
"description": "Controlla la tua versione di Deno e aggiorna all'ultima versione.",
"current_version": "Versione Corrente:",
"latest_version": "Ultima Versione:",
"status_idle": "Fai clic su 'Controlla Aggiornamenti' per iniziare",
"status_checking": "🔄 Controllo aggiornamenti...",
"status_up_to_date": "✓ Aggiornato",
"status_update_available": "⚠ Aggiornamento disponibile",
"status_not_installed": "✗ Non installato",
"check_updates": "Controlla Aggiornamenti",
"update_now": "Aggiorna Deno",
"updating": "🔄 Aggiornamento di Deno...",
"update_success": "✅ Deno è stato aggiornato con successo!",
"update_failed": "❌ Aggiornamento fallito: {error}",
"check_failed": "Impossibile controllare gli aggiornamenti. Controlla la tua connessione Internet."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "日本語 (Japanese)",
"select_language": "言語を選択:",
"current_language": "現在の言語: {language}",
"restart_required": "言語の変更はアプリケーションの再起動後に有効になります。",
"english": "英語",
"spanish": "スペイン語",
"portuguese": "ポルトガル語",
"russian": "ロシア語",
"chinese": "中国語",
"german": "ドイツ語",
"french": "フランス語",
"hindi": "ヒンディー語",
"indonesian": "インドネシア語",
"turkish": "トルコ語",
"polish": "ポーランド語",
"italian": "イタリア語",
"arabic": "アラビア語",
"japanese": "日本語",
"help_text": "インターフェースの言語を選択してください。",
"restart_notice": "言語の変更はアプリケーションの再起動後に有効になります。"
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "準備完了"
},
"formats": {
"show_formats": "フォーマットを表示:",
"video_format": "動画フォーマット:",
"audio_format": "音声フォーマット:",
"no_formats": "利用可能なフォーマットがありません",
"loading": "フォーマットを読み込み中...",
"select": "選択",
"quality": "品質",
"extension": "拡張子",
"resolution": "解像度",
"file_size": "ファイルサイズ",
"codec": "コーデック",
"audio": "音声",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "音声が結合されます",
"has_audio": "✓ 音声あり",
"audio_only": "音声のみ",
"best_4k": "最高 (4K)",
"best_2k": "最高 (2K)",
"high_1080p": "高 (1080p)",
"high_720p": "高 (720p)",
"medium_480p": "中 (480p)",
"low_quality": "低品質",
"best_audio": "最高音質",
"high_audio": "高音質",
"medium_audio": "中音質",
"low_audio": "低音質",
"audio_only_resolution": "音声のみ"
},
"buttons": {
"reset": "リセット",
"download": "ダウンロード",
"pause": "一時停止",
"resume": "再開",
"cancel": "キャンセル",
"browse": "参照",
"clear": "クリア",
"ok": "OK",
"apply": "適用",
"close": "閉じる",
"run_command": "コマンド実行",
"custom_command_help": "ヘルプ",
"about": "バージョン情報",
"analyze": "解析",
"paste_url": "URLを貼り付け",
"select_videos": "動画を選択...",
"video": "動画",
"audio_only": "音声のみ",
"custom_options": "カスタムオプション",
"trim_video": "動画をトリミング",
"download_settings": "ダウンロード設定",
"update": "yt-dlpを更新",
"select_defaults": "デフォルトを選択",
"select_all": "すべて選択",
"deselect_all": "すべて解除",
"open_folder": "フォルダの場所を開く",
"history": "履歴",
"save_playlist": "プレイリストを別名で保存"
},
"dialogs": {
"custom_options": "カスタムオプション",
"settings": "設定",
"select_folder": "ダウンロードフォルダを選択",
"sponsorblock_categories": "SponsorBlockカテゴリ",
"sponsorblock_description": "ダウンロード時に自動的に削除する動画セグメントの種類を選択してください。\nSponsorBlockはコミュニティ提供のデータを使用してこれらのセグメントを識別します。",
"select_subtitles": "字幕を選択",
"filter_languages_placeholder": "言語でフィルタ (例: ja, en)...",
"no_subtitles_available": "利用可能な字幕がありません",
"matching": "一致",
"ytdlp_log_title": "yt-dlpログ",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Cookieでログイン",
"custom_command": "カスタムコマンド",
"proxy": "プロキシ",
"language": "言語",
"updater": "アップデーター"
},
"cookies": {
"help_text": "認証用のCookieを提供する方法を選択してください。\nこれにより、プライベート動画や高品質の音声ファイルをダウンロードできます。",
"cookie_source": "Cookieソース",
"use_cookie_file": "Cookieファイルを使用",
"extract_from_browser": "ブラウザから抽出",
"recommended": "推奨",
"cookie_file": "Cookieファイル",
"cookie_file_placeholder": "cookies.txtへのパス...",
"browser_selection": "ブラウザ選択",
"browser_help": "Cookieを抽出するブラウザを選択してください:",
"browser_label": "ブラウザ:",
"profile_label": "プロファイル:",
"profile_placeholder": "デフォルト",
"browser_extract_message": "ブラウザのCookieは適用後に抽出されます",
"file_selected_message": "Cookieファイルが選択されました - OKをクリックして適用",
"select_file_title": "Cookieファイルを選択",
"file_filter": "Cookieファイル (*.txt *.lwp)",
"file_selected_title": "Cookieファイルが適用されました",
"file_applied_message": "Cookieファイルが適用されました: {path}",
"browser_selected_title": "ブラウザCookieが適用されました",
"browser_applied_message": "ブラウザCookieが抽出されます: {browser}",
"cleared_title": "Cookieがクリアされました",
"cleared_message": "Cookie設定がクリアされました",
"active_browser": "✓ 有効: ブラウザーのCookie ({browser})",
"active_file": "✓ 有効: Cookieファイル ({file})",
"none_active": "○ 有効なCookieはありません",
"remember_settings": "次回の起動時にCookie設定を記憶する"
},
"custom_command": {
"help_text": "以下にカスタムyt-dlpコマンドを入力してください。現在のURLは自動的に追加されます。<br><br>オプションの完全なリストと使用例については、<a href=\"{docs_url}\">こちらをクリックしてyt-dlp公式ドキュメントを参照してください</a>。<br><br>注: ダウンロードパスとファイル名テンプレートは自動的に処理されます。",
"input_label": "yt-dlp引数:",
"input_placeholder": "yt-dlp引数をここに入力...\n\n例: --extract-audio --audio-format mp3",
"output_label": "コマンド出力:",
"output_placeholder": "コマンド出力がここに表示されます...",
"command_placeholder": "カスタムyt-dlpコマンドを入力...",
"command_help": "使用可能なプレースホルダー:\n{url} - 動画URL\n{output} - 出力フォルダ\n\n例: --write-info-json --write-thumbnail",
"full_command": "🔧 完全なコマンド: {command}",
"command_success": "✅ カスタムコマンドが正常に実行されました!",
"command_failed": "❌ コマンドが終了コード{code}で失敗しました",
"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": "ダウンロード用のプロキシ設定を構成します。直接接続の場合は空白のままにしてください。",
"main_proxy": "メインプロキシ",
"main_proxy_help": "すべてのダウンロード用のメインプロキシサーバー。HTTP/HTTPSおよびSOCKS5プロトコルをサポートします。",
"proxy_url_label": "プロキシURL:",
"proxy_url_placeholder": "http://proxy:port または socks5://proxy:port",
"proxy_examples": "例:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "地域制限回避プロキシ",
"geo_proxy_help": "地域制限を回避するための追加プロキシ。",
"geo_proxy_url_label": "地域制限回避プロキシURL:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "メインプロキシをクリア",
"clear_geo_proxy": "地域プロキシをクリア",
"proxy_url": "プロキシURL",
"proxy_placeholder": "http://proxy:port または socks5://proxy:port",
"geo_bypass": "地域制限回避プロキシ (地域制限用)",
"geo_bypass_placeholder": "http://proxy:port 地域ブロック回避用",
"invalid_main_url": "メインプロキシのURL形式が無効です",
"invalid_geo_url": "地域プロキシのURL形式が無効です",
"main_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": "ダウンロードを準備中...",
"starting": "🚀 ダウンロードを開始しています...",
"fetching_info": "🔍 動画情報を取得中...",
"preparing_streams": "🎯 動画ストリームを準備中...",
"downloading_audio": "⏬ 音声をダウンロード中...",
"downloading_video": "⏬ 動画をダウンロード中...",
"downloading_subtitle": "⏬ 字幕をダウンロード中...",
"downloading": "⏬ ダウンロード中...",
"completed": "✅ ダウンロード完了!",
"video_completed": "✅ 動画ダウンロード完了!",
"audio_completed": "✅ 音声ダウンロード完了!",
"subtitle_completed": "✅ 字幕ダウンロード完了!",
"completed_cleaning": "✅ ダウンロード完了!クリーンアップ中...",
"cancelled": "ダウンロードがキャンセルされました",
"processing_playlist": "📋 再生リストデータを処理中...",
"paused": "ダウンロードが一時停止されました",
"resumed": "ダウンロードが再開されました",
"merging_formats": "✨ 後処理:フォーマットを結合中...",
"removing_sponsor_segments": "✨ 後処理:スポンサーセグメントを削除中...",
"speed": "速度",
"eta": "予想時間",
"please_enter_url": "最初にURLを入力してください。",
"please_set_path": "最初にダウンロードパスを設定してください。",
"please_enter_url_and_path": "URLを入力し、ダウンロードパスを設定してください。",
"please_select_format": "最初にフォーマットを選択してください。",
"downloading_fallback": "⚡ ダウンロード中..."
},
"update": {
"title": "yt-dlpを更新",
"checking": "アップデートを確認中...",
"update_available": "アップデートが利用可能です!\n現在のバージョン: {current}\n最新バージョン: {latest}",
"up_to_date": "yt-dlpは最新です (バージョン {version})",
"could_not_determine": "バージョンを確認できませんでした。",
"error_comparing": "バージョン比較エラー: {error}",
"update_available_failed": "アップデートが利用可能です!(比較失敗)\n現在: {current}\n最新: {latest}",
"updating": "更新中...",
"initializing": "🚀 更新プロセスを初期化中...",
"checking_current": "🔍 現在のインストールを確認中...",
"found_at": "📍 yt-dlpが見つかりました: {path}",
"error_getting_path": "❌ yt-dlpパスの取得エラー: {error}",
"updating_binary": "📦 アプリ管理のyt-dlpバイナリを更新中...",
"update_failed": "❌ yt-dlpの更新に失敗しました。もう一度試すか、インターネット接続を確認してください。",
"binary_updated": "✅ バイナリが正常に更新されました!",
"update_failed_stderr": "❌ yt-dlpの更新に失敗しました: {error}",
"update_timeout": "❌ yt-dlp更新がタイムアウトしました。",
"unexpected_error": "❌ 更新中に予期しないエラーが発生しました: {error}",
"already_up_to_date": "✅ yt-dlpはすでに最新です!",
"update_success": "✅ yt-dlpが正常に更新されました!",
"already_latest": "yt-dlpは最新です(バージョン {version}",
"network_error": "❌ 更新中にネットワークエラーが発生しました: {error}",
"general_error": "❌ 更新に失敗しました: {error}",
"update_in_progress_title": "更新中",
"update_in_progress_message": "yt-dlpを更新中です。少々お待ちください。"
},
"about": {
"title": "YTSageについて",
"version": "バージョン {version}",
"description": "クリーンなPySide6インターフェースを備えたモダンなYouTubeダウンローダー。",
"author": "作成者: {author}",
"github": "GitHub: {repo}",
"system_info": "システム情報",
"loading": "🔄 システム情報を読み込み中...",
"refresh": "🔄",
"open_logs": "📂 ログ",
"logs_tooltip": "アプリケーションのログフォルダを開く",
"refreshing": "🔄 更新中...",
"refresh_failed": "更新に失敗しました",
"refresh_failed_message": "バージョン情報を更新できませんでした。",
"detected": "✓ 検出済み",
"missing": "✗ 不足",
"not_available": "利用不可"
},
"time_range": {
"title": "動画をトリミング",
"time_range_group": "時間範囲",
"start_time": "開始時間 (HH:MM:SS)",
"end_time": "終了時間 (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "開始時間 (HH:MM:SS)",
"end_time_placeholder": "終了時間 (HH:MM:SS)",
"force_keyframes": "カットポイントでキーフレームを強制",
"help_text": "動画をトリミングするには開始時間と終了時間を設定してください。\n完全な動画をダウンロードする場合は空白のままにしてください。",
"invalid_format": "時間フォーマットが無効です。HH:MM:SSフォーマットを使用してください。",
"start_after_end": "開始時間を終了時間より後にすることはできません。"
},
"settings": {
"title": "ダウンロード設定",
"download_path": "ダウンロードパス",
"browse": "参照...",
"speed_limit": "速度制限",
"speed_limit_placeholder": "なし",
"generic_mode": "汎用モード",
"enable_generic_mode": "汎用モードを有効化(YouTube以外のサイトをサポート)",
"generic_mode_help": "Dailymotion、CBC Gem、および yt-dlp が対応する他のサイトからのダウンロードを可能にします。",
"auto_update_ytdlp": "yt-dlp自動更新",
"enable_auto_updates": "yt-dlp自動更新を有効化",
"update_frequency": "更新頻度:",
"check_startup": "起動時に毎回確認 (チェック間隔は最低1時間)",
"check_daily": "毎日確認",
"check_weekly": "毎週確認",
"check_updates_now": "yt-dlpを更新",
"update_check_title": "更新確認",
"could_not_determine_version": "yt-dlpの現在のバージョンを確認できませんでした。",
"update_available_dialog": "アップデートが利用可能です!\n\n現在: {current}\n最新: {latest}\n\nOKをクリックして更新を続行します。",
"up_to_date_dialog": "yt-dlpは最新です!\n\n現在のバージョン: {version}",
"error_checking_updates": "更新確認エラー: {error}",
"settings_saved_title": "設定を保存しました",
"settings_saved_message": "自動更新設定が正常に保存されました!",
"error_title": "エラー",
"failed_save_settings": "自動更新設定の保存に失敗しました。",
"error_saving_settings": "自動更新設定の保存エラー: {error}",
"ytdlp_channel": "yt-dlp リリースチャンネル",
"ytdlp_channel_stable": "安定版(テスト済みリリース)",
"ytdlp_channel_nightly": "Nightly(毎日更新、yt-dlp推奨)",
"ytdlp_channel_description": "安定版とNightlyリリースのどちらかを選択してください。Nightlyビルドは最新の修正と機能で毎日更新されます。いつでもチャンネルを切り替えることができます。",
"ytdlp_switching_channel": "{channel}チャンネルに切り替え中...",
"ytdlp_channel_switched": "✅ {channel}チャンネルへの切り替えに成功しました!",
"ytdlp_channel_switch_failed": "❌ チャンネルの切り替えに失敗しました: {error}",
"ytdlp_current_channel": "現在のチャンネル: {channel}",
"app_updates_title": "YTSageの更新",
"check_app_updates": "起動時にYTSageの更新を確認する",
"check_beta_updates": "ベータ版の更新を受け取る",
"auto_update_title": "自動更新設定",
"auto_update_header": "🔄 自動更新設定",
"auto_update_description": "yt-dlpの自動更新を設定して、最新の機能とバグ修正を確実に入手してください。",
"current_status": "現在のステータス",
"current_version_label": "現在のyt-dlpバージョン: 確認中...",
"last_check_label": "最終更新確認: なし",
"next_check_label": "次回確認: 設定に基づく",
"manual_check_button": "🔍 今すぐ更新を確認",
"update_frequency_group": "更新頻度",
"save_settings": "設定を保存",
"settings_saved_successfully": "✅ 設定が正常に保存されました!",
"error_saving": "❌ 設定の保存エラー: {error}",
"output_format_settings": "出力形式設定",
"force_output_format": "マージ時に出力形式を強制",
"preferred_format": "優先形式:",
"force_format_help": "有効にすると、マージされた動画は優先形式に変換されます。無効にすると、yt-dlpが自動的に決定します。",
"format_mp4": "MP4(最も互換性が高い)",
"format_webm": "WebM(モダン、オープン)",
"format_mkv": "MKV(機能豊富)",
"audio_format_settings": "オーディオ形式設定",
"force_audio_format": "オーディオのみのダウンロードにオーディオ形式を強制",
"audio_normalization": "オーディオ正規化 (EBU R128)",
"audio_normalization_help": "有効にすると、オーディオトラックが正規化されます。注:これには再エンコードが必要なため、特定のオーディオ形式(MP3やM4Aなど)を強制する必要があります。",
"preferred_audio_format": "優先オーディオ形式:",
"force_audio_format_help": "有効にすると、オーディオのみのダウンロードが優先形式に変換されます。これはオーディオ形式をダウンロードする場合にのみ適用されます。",
"audio_format_best": "最高(変換なし)",
"audio_format_aac": "AAC(編集に適している)",
"audio_format_mp3": "MP3(汎用)",
"audio_format_flac": "FLAC(ロスレス)",
"audio_format_wav": "WAV(非圧縮)",
"audio_format_opus": "Opus(効率的)",
"audio_format_m4a": "M4AApple",
"audio_format_vorbis": "Vorbis(オープン)",
"filename_format": "出力ファイル名の形式",
"filename_format_help": "利用可能な変数: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s。標準のyt-dlp出力テンプレート構文がサポートされています。",
"tab_general": "一般",
"tab_format": "フォーマット",
"tab_file": "ファイル",
"concurrent_fragments": "同時接続数",
"concurrent_fragments_help": "ダウンロードごとの接続数。値を大きくするとスロットリングを回避できますが、高すぎると一時的なブロックが発生する可能性があります。デフォルト: 1。",
"defaults_settings": "デフォルト選択設定",
"default_video_quality": "デフォルトのビデオ解像度 (高さ):",
"default_subtitle_language": "デフォルトの字幕言語:",
"defaults_help": "優先するビデオの高さと字幕言語(カンマ区切り)を設定します。利用可能な場合は自動的に選択されます。"
},
"main_ui": {
"url_placeholder": "YouTubeの動画URLまたはプレイリストURLを入力",
"url_placeholder_generic": "対応サイトの動画またはプレイリストURLを入力",
"merge_subtitles": "字幕を結合",
"save_thumbnail": "サムネイルを保存",
"save_description": "説明を保存",
"embed_chapters": "チャプターを埋め込む",
"subtitles_selected": "{count}個選択",
"all_selected": "すべて選択済み",
"select_videos_all": "動画を選択... (すべて選択済み)",
"please_enter_url": "最初にURLを入力してください",
"error_title": "エラー",
"cookie_file_selected_title": "Cookieファイルが選択されました",
"cookie_file_selected_message": "選択されたCookieファイル: {path}",
"browser_cookies_selected_title": "ブラウザCookieが選択されました",
"browser_cookies_selected_message": "ブラウザCookieが抽出されます: {browser}",
"error_no_format_info": "エラー: 利用可能なフォーマット情報がありません。",
"error_extract_info": "エラー: 基本的な動画情報を抽出できませんでした。リンクを確認してください。",
"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": "情報を抽出中...",
"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": "最初に字幕を選択してください",
"settings_tooltip": "現在のパス: {path}\n速度制限: {speed_limit}",
"speed_limit_none": "なし",
"open_folder_error": "フォルダーを開けませんでした: {error}",
"time_range_set": "セクション設定: {section}"
},
"sponsorblock": {
"sponsor": "スポンサー",
"sponsor_desc": "有料広告、有料推薦、直接広告",
"selfpromo": "無報酬/自己宣伝",
"selfpromo_desc": "クリエイター自身のコンテンツの無報酬プロモーション",
"interaction": "インタラクションリマインダー",
"interaction_desc": "視聴者に「いいね」、登録、ソーシャルメディアでのフォローを求める",
"intro": "イントロ",
"intro_desc": "スキップ可能な動画のイントロ",
"outro": "アウトロ/エンドカード",
"outro_desc": "エンドクレジットまたは動画の終了",
"preview": "プレビュー/要約",
"preview_desc": "前回の動画の短い要約または今後のコンテンツのプレビュー",
"music_offtopic": "非音楽セクション",
"music_offtopic_desc": "音楽動画のみ。非音楽セクションを示す",
"filler": "余談フィラー",
"filler_desc": "フィラーやユーモアのためだけに追加された余談シーン"
},
"video_info": {
"channel": "チャンネル",
"views": "👁 視聴回数",
"likes": "👍 いいね",
"upload_date": "📅 アップロード日",
"duration": "⏱ 長さ",
"unknown_channel": "不明なチャンネル",
"unknown_date": "不明な日付",
"unknown_title": "不明なタイトル"
},
"command": {
"running": "実行中...",
"run_command": "コマンド実行"
},
"selection": {
"none_selected": "0個選択",
"one_selected": "1カテゴリ選択",
"count_selected": "{count}個選択"
},
"status": {
"ready": "準備完了",
"file_exists": "⚠️ ファイルがすでに存在します",
"video_file_exists": "⚠️ 動画ファイルがすでに存在します",
"audio_file_exists": "⚠️ 音声ファイルがすでに存在します",
"subtitle_file_exists": "⚠️ 字幕ファイルがすでに存在します",
"cancelling": "ダウンロードをキャンセル中...",
"thumbnail_saved": "✅ サムネイルを保存しました: {filename}",
"thumbnail_error": "❌ サムネイルエラー: {error}",
"thumbnail_no_image": "保存できるサムネイルがありません"
},
"errors": {
"playlist_no_videos": "エラー: 再生リストに有効な動画が含まれていません。",
"playlist_no_url": "エラー: 再生リストの最初の動画のURLを取得できませんでした。",
"ytdlp_not_found": "エラー: yt-dlp実行ファイルが見つかりません。最初にyt-dlpをインストールしてください。",
"ytdlp_not_found_path": "エラー: yt-dlp実行ファイルが見つかりません。不適切なインストールまたはPATHの問題が原因の可能性があります。",
"no_data_returned": "エラー: yt-dlpからデータが返されませんでした",
"no_format_info": "エラー: 利用可能なフォーマット情報がありません。",
"analysis_timeout": "エラー: 解析がタイムアウトしました。もう一度試してください。",
"invalid_speed_limit": "❌ エラー: 設定で無効な速度制限値が設定されています。",
"ytdlp_failed": "エラー: yt-dlpが失敗しました: {error}",
"parse_failed": "エラー: yt-dlp出力の解析に失敗しました: {error}",
"analysis_failed": "エラー: 解析が失敗しました: {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}",
"private_video": "この動画は非公開かもしれません。カスタムオプションからCookieを使用してください。"
},
"update_dialog": {
"title": "アップデートが利用可能です",
"new_version_available": "YTSageの新しいバージョンが利用可能です!",
"current_version_label": "現在のバージョン:",
"latest_version_label": "最新バージョン:",
"changelog": "変更履歴",
"download_update": "更新をダウンロード",
"remind_later": "後で通知"
},
"playlist": {
"unknown": "不明な再生リスト",
"total_videos": "総動画数: {count}",
"display_format": "再生リスト: {title} | {count}個の動画",
"select_videos_title": "プレイリスト動画を選択",
"save_as": "プレイリストを別名で保存",
"save_success_title": "成功",
"saved_successfully": "プレイリストを正常に保存しました。",
"save_error_title": "保存エラー",
"no_videos_to_save": "プレイリストのエントリが見つかりません!",
"save_error_msg": "プレイリストの保存に失敗しました。"
},
"subtitle_selection": {
"count_selected": "{count}個選択"
},
"file_exists_dialog": {
"title": "ファイルが既に存在します",
"message": "ファイルが既に存在します:\n{filename}",
"info": "この動画は既にダウンロードされています。"
},
"auto_update": {
"last_check_never": "最終更新確認: なし",
"last_check": "最終更新確認: {time}",
"next_check_disabled": "次回確認: 無効",
"next_check_startup": "次回確認: 起動時",
"next_check_overdue": "次回確認: 今すぐ (遅延)",
"next_check": "次回確認: {time}",
"next_check_error": "次回確認: 計算エラー",
"checking": "🔄 確認中...",
"check_now": "🔍 今すぐ更新を確認",
"current_version": "現在のyt-dlpバージョン: {version}"
},
"url_validation": {
"empty_url": "URLを空にすることはできません",
"invalid_format": "無効なURL形式",
"invalid_scheme": "URLはhttp://またはhttps://で始まる必要があります",
"missing_domain": "無効なURL:ドメイン名がありません",
"unsupported_platform": "YTSageはYouTubeとYouTube MusicのURLのみをサポートしています。\nドメイン'{domain}'はサポートされていません。",
"invalid_youtu_be": "無効なyoutu.be URL:ビデオIDがありません"
},
"ytdlp_errors": {
"private_video": "これは非公開動画です。Cookieを使用してアカウントにログインすることでダウンロードできます。\n認証するには、「カスタムオプション」→「Cookieでログイン」→「ブラウザからCookieを抽出」に移動してください。",
"age_restricted": "この動画は年齢制限があります。アクセスするにはログインする必要があります。\nアカウントで認証するには、「カスタムオプション」→「Cookieでログイン」を使用してください。",
"geo_blocked": "この動画はお住まいの地域では利用できません(地理的にブロックされています)。\nVPNを使用する必要がある場合や、お住まいの国で動画が制限されている可能性があります。",
"video_unavailable": "この動画は削除されたか、利用できなくなりました。\n動画はアップローダーによって削除されたか、ポリシー違反のために削除された可能性があります。",
"live_stream": "これはアクティブな間はダウンロードできないライブストリームです。\nストリームが終了するまで待ってから、アーカイブされたバージョンのダウンロードを試してください。",
"playlist_error": "このプレイリストにアクセスできません。非公開、削除、または空の可能性があります。\nプレイリストが存在し、公開されているか確認してください。",
"network_error": "ネットワーク接続エラー。インターネット接続を確認して、もう一度お試しください。\n問題が解決しない場合は、ビデオサーバーが一時的に利用できない可能性があります。",
"invalid_url": "無効またはサポートされていないURL。リンクを確認して、もう一度お試しください。\n有効なYouTube、Vimeo、またはその他のサポートされているプラットフォームのURLを使用していることを確認してください。",
"premium_content": "このコンテンツにはYouTube Premiumまたはチャンネルメンバーシップが必要です。\nこのコンテンツにアクセスできるアカウントでログインする必要があります。",
"copyright_blocked": "著作権の申し立てにより、この動画はブロックされています。\nコンテンツ所有者がこの動画へのアクセスを制限しています。",
"extraction_failed": "動画情報の抽出に失敗しました。これは一時的な問題である可能性があります。\n数分後にもう一度お試しいただくか、動画リンクが正しいか確認してください。",
"generic_error": "動画情報を抽出できませんでした。リンクを確認してください。\n技術的詳細:{error}"
},
"history": {
"title": "ダウンロード履歴",
"clear_all": "すべてクリア",
"clear_confirm_title": "履歴をクリア?",
"clear_confirm_message": "本当によろしいですか?この操作は取り消せません。",
"no_history": "まだ履歴がありません",
"no_history_description": "ダウンロードがここに表示されます",
"loading": "履歴を読み込み中...",
"search_placeholder": "検索...",
"open_location": "場所を開く",
"redownload": "再ダウンロード",
"remove": "削除",
"file_not_found": "ファイルが見つかりません",
"file_not_found_message": "ファイルが移動または削除されました:\n{path}",
"downloaded_on": "ダウンロード日:{date}",
"file_size": "サイズ:{size}",
"audio_download": "オーディオ",
"video_download": "ビデオ",
"remove_confirm_title": "削除?",
"remove_confirm_message": "履歴から削除しますか?\n\n{title}",
"item_removed": "削除しました",
"history_cleared": "履歴をクリアしました",
"entries_count": "{count} 件",
"one_entry": "1 件",
"redownload_confirm_title": "再ダウンロード?",
"redownload_confirm_message": "再ダウンロードしますか?\n\n{title}",
"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バージョンチェッカー",
"current_version": "現在のバージョン:",
"latest_version": "最新バージョン:",
"status_up_to_date": "✓ 最新",
"status_update_available": "⚠ 更新可能",
"status_not_installed": "✗ 未インストール",
"status_idle": "開始するには「バージョンを確認」をクリック",
"status_checking": "🔄 バージョンを確認中...",
"check_updates": "バージョンを確認",
"check_failed": "バージョンの確認に失敗しました。インターネット接続を確認してください。",
"description": "FFmpegのバージョンを確認し、利用可能な最新バージョンと比較します。",
"guide_info": " FFmpegをインストールまたは更新するには、<a href='https://github.com/oop7/ffmpeg-install-guide'>こちらをクリックして包括的なインストールガイドをご覧ください</a>。"
},
"deno": {
"setup_required": "Denoのセットアップが必要です",
"setup_description": "YTSageは特定の機能を実行するためにDenoが必要です。<br><br>アプリのローカルディレクトリにDenoが見つかりませんでした。YTSageはお使いの{os_name}システム用にDenoをセットアップする必要があります。<br><br>「Denoをセットアップ」をクリックして、自動的にダウンロードとインストールを行います。",
"setup_button": "Denoをセットアップ",
"downloading": "Denoをダウンロード中...",
"extracting": "Denoを展開中...",
"verifying": "Denoを検証中...",
"success": "Denoが正常にインストールされました!",
"download_failed": "ダウンロード失敗",
"download_error": "Denoのダウンロードに失敗しました:{error}",
"verification_failed": "SHA256検証に失敗しました。ダウンロードされたファイルが破損しているか、改ざんされている可能性があります。",
"setup_failed": "Denoのセットアップに失敗しました。一部の機能が正しく動作しない可能性があります。",
"setup_error": "セットアップエラー"
},
"deno_updater": {
"title": "Denoバージョンチェッカーとアップデーター",
"description": "Denoのバージョンを確認し、最新バージョンに更新します。",
"current_version": "現在のバージョン:",
"latest_version": "最新バージョン:",
"status_idle": "開始するには「更新を確認」をクリックしてください",
"status_checking": "🔄 更新を確認中...",
"status_up_to_date": "✓ 最新",
"status_update_available": "⚠ 更新が利用可能",
"status_not_installed": "✗ インストールされていません",
"check_updates": "更新を確認",
"update_now": "Denoを更新",
"updating": "🔄 Denoを更新中...",
"update_success": "✅ Denoが正常に更新されました!",
"update_failed": "❌ 更新が失敗しました:{error}",
"check_failed": "更新の確認に失敗しました。インターネット接続を確認してください。"
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Polski (Polish)",
"select_language": "Wybierz język:",
"current_language": "Aktualny język: {language}",
"restart_required": "Zmiana języka zostanie zastosowana po ponownym uruchomieniu aplikacji.",
"english": "Angielski",
"spanish": "Hiszpański",
"portuguese": "Portugalski",
"russian": "Rosyjski",
"chinese": "Chiński",
"german": "Niemiecki",
"french": "Francuski",
"hindi": "Hindi",
"indonesian": "Indonezyjski",
"turkish": "Turecki",
"polish": "Polski",
"italian": "Włoski",
"arabic": "Arabski",
"japanese": "Japoński",
"help_text": "Wybierz preferowany język interfejsu.",
"restart_notice": "Zmiana języka zostanie zastosowana po ponownym uruchomieniu aplikacji."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Gotowy"
},
"formats": {
"show_formats": "Pokaż formaty:",
"video_format": "Format wideo:",
"audio_format": "Format dźwięku:",
"no_formats": "Brak dostępnych formatów",
"loading": "Ładowanie formatów...",
"select": "Wybierz",
"quality": "Jakość",
"extension": "Rozszerzenie",
"resolution": "Rozdzielczość",
"file_size": "Rozmiar pliku",
"codec": "Kodek",
"audio": "Dźwięk",
"fps": "KL/S",
"hdr": "HDR",
"will_merge_audio": "Dźwięk zostanie połączony",
"has_audio": "✓ Zawiera dźwięk",
"audio_only": "Tylko dźwięk",
"best_4k": "Najlepsza (4K)",
"best_2k": "Najlepsza (2K)",
"high_1080p": "Wysoka (1080p)",
"high_720p": "Wysoka (720p)",
"medium_480p": "Średnia (480p)",
"low_quality": "Niska jakość",
"best_audio": "Najlepszy dźwięk",
"high_audio": "Wysoki dźwięk",
"medium_audio": "Średni dźwięk",
"low_audio": "Niski dźwięk",
"audio_only_resolution": "Tylko dźwięk"
},
"buttons": {
"reset": "Zresetuj",
"download": "Pobierz",
"pause": "Wstrzymaj",
"resume": "Wznów",
"cancel": "Anuluj",
"browse": "Przeglądaj",
"clear": "Wyczyść",
"ok": "OK",
"apply": "Zastosuj",
"close": "Zamknij",
"run_command": "Uruchom polecenie",
"custom_command_help": "Pomoc",
"about": "O programie",
"analyze": "Analizuj",
"paste_url": "Wklej URL",
"select_videos": "Wybierz wideo...",
"video": "Wideo",
"audio_only": "Tylko dźwięk",
"custom_options": "Opcje niestandardowe",
"trim_video": "Przytnij wideo",
"download_settings": "Ustawienia pobierania",
"update": "Zaktualizuj yt-dlp",
"select_defaults": "Wybierz domyślne",
"select_all": "Zaznacz wszystko",
"deselect_all": "Odznacz wszystko",
"open_folder": "Otwórz lokalizację folderu",
"history": "Historia",
"save_playlist": "Zapisz playlistę jako"
},
"dialogs": {
"custom_options": "Opcje niestandardowe",
"settings": "Ustawienia",
"select_folder": "Wybierz folder pobierania",
"sponsorblock_categories": "Kategorie SponsorBlock",
"sponsorblock_description": "Wybierz typy segmentów wideo do automatycznego usunięcia podczas pobierania.\nSponsorBlock używa danych przesyłanych przez społeczność do identyfikacji tych segmentów.",
"select_subtitles": "Wybierz napisy",
"filter_languages_placeholder": "Filtruj języki (np: pl, en)...",
"no_subtitles_available": "Brak dostępnych napisów",
"matching": "dopasowujące",
"ytdlp_log_title": "Log yt-dlp",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Zaloguj za pomocą ciasteczek",
"custom_command": "Polecenie niestandardowe",
"proxy": "Proxy",
"language": "Język",
"updater": "Aktualizator"
},
"cookies": {
"help_text": "Wybierz sposób dostarczania ciasteczek do uwierzytelniania.\nUmożliwia to pobieranie prywatnych filmów i wysokiej jakości plików audio.",
"cookie_source": "Źródło ciasteczek",
"use_cookie_file": "Użyj pliku ciasteczek",
"extract_from_browser": "Wyodrębnij z przeglądarki",
"recommended": "Zalecane",
"cookie_file": "Plik ciasteczek",
"cookie_file_placeholder": "Ścieżka do pliku cookies.txt...",
"browser_selection": "Wybór przeglądarki",
"browser_help": "Wybierz przeglądarkę do wyodrębnienia ciasteczek:",
"browser_label": "Przeglądarka:",
"profile_label": "Profil:",
"profile_placeholder": "Domyślny",
"browser_extract_message": "Ciasteczka przeglądarki zostaną wyodrębnione po zastosowaniu",
"file_selected_message": "Wybrano plik ciasteczek - Kliknij OK, aby zastosować",
"select_file_title": "Wybierz plik ciasteczek",
"file_filter": "Pliki ciasteczek (*.txt *.lwp)",
"file_selected_title": "Zastosowano plik ciasteczek",
"file_applied_message": "Zastosowano plik ciasteczek: {path}",
"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",
"active_browser": "✓ Aktywne: ciasteczka przeglądarki ({browser})",
"active_file": "✓ Aktywny: plik ciasteczek ({file})",
"none_active": "○ Brak aktywnych ciasteczek",
"remember_settings": "Zapamiętaj ustawienia plików cookie przy następnym uruchomieniu"
},
"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.",
"input_label": "Argumenty yt-dlp:",
"input_placeholder": "Wprowadź argumenty yt-dlp tutaj...\n\nPrzykład: --extract-audio --audio-format mp3",
"output_label": "Wynik polecenia:",
"output_placeholder": "Wynik polecenia pojawi się tutaj...",
"command_placeholder": "Wprowadź niestandardowe polecenie yt-dlp...",
"command_help": "Dostępne placeholdery:\n{url} - URL wideo\n{output} - Folder wyjściowy\n\nPrzykład: --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Główny proxy",
"main_proxy_help": "Główny serwer proxy dla wszystkich pobierań. Obsługuje protokoły HTTP/HTTPS i SOCKS5.",
"proxy_url_label": "URL Proxy:",
"proxy_url_placeholder": "http://proxy:port lub socks5://proxy:port",
"proxy_examples": "Przykłady:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Proxy omijania geograficznego",
"geo_proxy_help": "Dodatkowy proxy specjalnie do omijania ograniczeń geograficznych.",
"geo_proxy_url_label": "URL proxy omijania geograficznego:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Wyczyść główny proxy",
"clear_geo_proxy": "Wyczyść proxy geograficzny",
"proxy_url": "URL Proxy",
"proxy_placeholder": "http://proxy:port lub socks5://proxy:port",
"geo_bypass": "Proxy omijania geograficznego (dla ograniczeń geograficznych)",
"geo_bypass_placeholder": "http://proxy:port do omijania geo-blokowania",
"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",
"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...",
"starting": "🚀 Rozpoczynanie pobierania...",
"fetching_info": "🔍 Pobieranie informacji o wideo...",
"preparing_streams": "🎯 Przygotowywanie strumieni wideo...",
"downloading_audio": "⏬ Pobieranie dźwięku...",
"downloading_video": "⏬ Pobieranie wideo...",
"downloading_subtitle": "⏬ Pobieranie napisów...",
"downloading": "⏬ Pobieranie...",
"completed": "✅ Pobieranie zakończone!",
"video_completed": "✅ Pobieranie wideo zakończone!",
"audio_completed": "✅ Pobieranie dźwięku zakończone!",
"subtitle_completed": "✅ Pobieranie napisów zakończone!",
"completed_cleaning": "✅ Pobieranie zakończone! Czyszczenie...",
"cancelled": "Pobieranie anulowane",
"processing_playlist": "📋 Przetwarzanie danych playlisty...",
"paused": "Pobieranie wstrzymane",
"resumed": "Pobieranie wznowione",
"merging_formats": "✨ Przetwarzanie końcowe: Łączenie formatów...",
"removing_sponsor_segments": "✨ Przetwarzanie końcowe: Usuwanie segmentów sponsorowanych...",
"speed": "Prędkość",
"eta": "Pozostały czas",
"please_enter_url": "Proszę najpierw wprowadzić URL.",
"please_set_path": "Proszę najpierw ustawić ścieżkę pobierania.",
"please_enter_url_and_path": "Proszę wprowadzić URL i ustawić ścieżkę pobierania.",
"please_select_format": "Proszę najpierw wybrać format.",
"downloading_fallback": "⚡ Pobieranie..."
},
"update": {
"title": "Zaktualizuj yt-dlp",
"checking": "Sprawdzanie aktualizacji...",
"update_available": "Dostępna aktualizacja!\nAktualna wersja: {current}\nNajnowsza wersja: {latest}",
"up_to_date": "yt-dlp jest aktualny (Wersja {version})",
"could_not_determine": "Nie można określić wersji.",
"error_comparing": "Błąd porównywania wersji: {error}",
"update_available_failed": "Dostępna aktualizacja! (Porównanie nie powiodło się)\nAktualna: {current}\nNajnowsza: {latest}",
"updating": "Aktualizowanie...",
"initializing": "🚀 Inicjalizacja procesu aktualizacji...",
"checking_current": "🔍 Sprawdzanie aktualnej instalacji...",
"found_at": "📍 yt-dlp znaleziony w: {path}",
"error_getting_path": "❌ Błąd pobierania ścieżki yt-dlp: {error}",
"updating_binary": "📦 Aktualizowanie binarki yt-dlp zarządzanej przez aplikację...",
"update_failed": "❌ Aktualizacja yt-dlp nie powiodła się. Spróbuj ponownie lub sprawdź połączenie internetowe.",
"binary_updated": "✅ Binarka została pomyślnie zaktualizowana!",
"update_failed_stderr": "❌ Aktualizacja yt-dlp nie powiodła się: {error}",
"update_timeout": "❌ Limit czasu aktualizacji yt-dlp.",
"unexpected_error": "❌ Nieoczekiwany błąd podczas aktualizacji: {error}",
"already_up_to_date": "✅ yt-dlp jest już aktualny!",
"update_success": "✅ yt-dlp został pomyślnie zaktualizowany!",
"already_latest": "yt-dlp jest aktualny (wersja {version})",
"network_error": "❌ Błąd sieci podczas aktualizacji: {error}",
"general_error": "❌ Aktualizacja nie powiodła się: {error}",
"update_in_progress_title": "Trwa aktualizacja",
"update_in_progress_message": "Trwa aktualizacja yt-dlp. Proszę chwilę zaczekać."
},
"about": {
"title": "O YTSage",
"version": "Wersja {version}",
"description": "Nowoczesny pobieracz YouTube z czystym interfejsem PySide6.",
"author": "Autor: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Wykryto",
"missing": "✗ Brakuje",
"not_available": "Niedostępne"
},
"time_range": {
"title": "Przytnij wideo",
"time_range_group": "Zakres czasu",
"start_time": "Czas rozpoczęcia (GG:MM:SS)",
"end_time": "Czas zakończenia (GG:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Czas rozpoczęcia (GG:MM:SS)",
"end_time_placeholder": "Czas zakończenia (GG:MM:SS)",
"force_keyframes": "Wymuś klatki kluczowe w punktach cięcia",
"help_text": "Ustaw czasy rozpoczęcia i zakończenia, aby przyciąć wideo.\nPozostaw puste, aby pobrać pełne wideo.",
"invalid_format": "Nieprawidłowy format czasu. Użyj formatu GG:MM:SS.",
"start_after_end": "Czas rozpoczęcia nie może być po czasie zakończenia."
},
"settings": {
"title": "Ustawienia pobierania",
"download_path": "Ścieżka pobierania",
"browse": "Przeglądaj...",
"speed_limit": "Limit prędkości",
"speed_limit_placeholder": "Brak",
"generic_mode": "Tryb ogólny",
"enable_generic_mode": "Włącz tryb ogólny (obsługa stron innych niż YouTube)",
"generic_mode_help": "Umożliwia pobieranie z Dailymotion, CBC Gem i innych stron obsługiwanych przez yt-dlp.",
"auto_update_ytdlp": "Automatyczne aktualizacje yt-dlp",
"enable_auto_updates": "Włącz automatyczne aktualizacje yt-dlp",
"update_frequency": "Częstotliwość aktualizacji:",
"check_startup": "Sprawdzaj przy każdym uruchomieniu (minimum 1 godzina między sprawdzeniami)",
"check_daily": "Sprawdzaj codziennie",
"check_weekly": "Sprawdzaj co tydzień",
"check_updates_now": "Zaktualizuj yt-dlp",
"update_check_title": "Sprawdzanie aktualizacji",
"could_not_determine_version": "Nie można określić aktualnej wersji yt-dlp.",
"update_available_dialog": "Dostępna aktualizacja!\n\nAktualna: {current}\nNajnowsza: {latest}\n\nKliknij OK, aby kontynuować aktualizację.",
"up_to_date_dialog": "yt-dlp jest aktualny!\n\nAktualna wersja: {version}",
"error_checking_updates": "Błąd sprawdzania aktualizacji: {error}",
"settings_saved_title": "Ustawienia zapisane",
"settings_saved_message": "Ustawienia automatycznych aktualizacji zostały pomyślnie zapisane!",
"error_title": "Błąd",
"failed_save_settings": "Nie udało się zapisać ustawień automatycznych aktualizacji.",
"error_saving_settings": "Błąd zapisywania ustawień automatycznych aktualizacji: {error}",
"ytdlp_channel": "Kanał wydań yt-dlp",
"ytdlp_channel_stable": "Stabilny (Przetestowane wydania)",
"ytdlp_channel_nightly": "Nightly (Codzienne aktualizacje, zalecane przez yt-dlp)",
"ytdlp_channel_description": "Wybierz między wydaniami stabilnymi a nightly. Buildy nightly są aktualizowane codziennie z najnowszymi poprawkami i funkcjami. Możesz zmienić kanał w dowolnym momencie.",
"ytdlp_switching_channel": "Przełączanie na kanał {channel}...",
"ytdlp_channel_switched": "✅ Pomyślnie przełączono na kanał {channel}!",
"ytdlp_channel_switch_failed": "❌ Nie udało się przełączyć kanału: {error}",
"ytdlp_current_channel": "Aktualny kanał: {channel}",
"app_updates_title": "Aktualizacje YTSage",
"check_app_updates": "Sprawdzaj aktualizacje YTSage przy uruchomieniu",
"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.",
"current_status": "Aktualny status",
"current_version_label": "Aktualna wersja yt-dlp: Sprawdzanie...",
"last_check_label": "Ostatnie sprawdzenie aktualizacji: Nigdy",
"next_check_label": "Następne sprawdzenie: Na podstawie ustawień",
"manual_check_button": "🔍 Sprawdź aktualizacje teraz",
"update_frequency_group": "Częstotliwość aktualizacji",
"save_settings": "Zapisz ustawienia",
"settings_saved_successfully": "✅ Ustawienia zapisane pomyślnie!",
"error_saving": "❌ Błąd zapisywania ustawień: {error}",
"output_format_settings": "Ustawienia formatu wyjściowego",
"force_output_format": "Wymuś format wyjściowy podczas łączenia",
"preferred_format": "Preferowany format:",
"force_format_help": "Po włączeniu połączone filmy zostaną przekonwertowane na wybrany format. Wyłącz, aby pozwolić yt-dlp zdecydować automatycznie.",
"format_mp4": "MP4 (Najbardziej kompatybilny)",
"format_webm": "WebM (Nowoczesny, otwarty)",
"format_mkv": "MKV (Bogaty w funkcje)",
"audio_format_settings": "Ustawienia formatu audio",
"force_audio_format": "Wymuś format audio dla pobierania tylko audio",
"audio_normalization": "Normalizacja dźwięku (EBU R128)",
"audio_normalization_help": "Po włączeniu ścieżki dźwiękowe zostaną znormalizowane. Uwaga: Wymaga to ponownego kodowania, więc należy wymusić określony format dźwięku (taki jak MP3 lub M4A).",
"preferred_audio_format": "Preferowany format audio:",
"force_audio_format_help": "Gdy włączone, pobieranie tylko audio zostanie przekonwertowane na preferowany format. Dotyczy to tylko pobierania formatów audio.",
"audio_format_best": "Najlepsze (Bez konwersji)",
"audio_format_aac": "AAC (Dobre do edycji)",
"audio_format_mp3": "MP3 (Uniwersalny)",
"audio_format_flac": "FLAC (Bezstratny)",
"audio_format_wav": "WAV (Nieskompresowany)",
"audio_format_opus": "Opus (Wydajny)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "Ogólne",
"tab_format": "Format",
"tab_file": "Plik",
"concurrent_fragments": "Jednoczesne połączenia",
"concurrent_fragments_help": "Liczba połączeń na pobieranie. Wyższe wartości omijają dławienie, ale mogą powodować tymczasowe blokady, jeśli zostaną ustawione zbyt wysoko. Domyślnie: 1.",
"defaults_settings": "Domyślne ustawienia wyboru",
"default_video_quality": "Domyślna rozdzielczość wideo (wysokość):",
"default_subtitle_language": "Domyślny język(i) napisów:",
"defaults_help": "Ustaw preferowaną wysokość wideo i języki napisów (rozdzielone przecinkami). Zostaną one automatycznie wybrane, jeśli będą dostępne."
},
"main_ui": {
"url_placeholder": "Wprowadź URL wideo YouTube lub playlisty",
"url_placeholder_generic": "Wprowadź URL filmu lub playlisty z dowolnej obsługiwanej strony",
"merge_subtitles": "Połącz napisy",
"save_thumbnail": "Zapisz miniaturę",
"save_description": "Zapisz opis",
"embed_chapters": "Osadź rozdziały",
"subtitles_selected": "{count} wybrano",
"all_selected": "Wszystkie wybrane",
"select_videos_all": "Wybierz wideo... (Wszystkie wybrane)",
"please_enter_url": "Proszę najpierw wprowadzić URL",
"error_title": "Błąd",
"cookie_file_selected_title": "Wybrano plik ciasteczek",
"cookie_file_selected_message": "Wybrano plik ciasteczek: {path}",
"browser_cookies_selected_title": "Wybrano ciasteczka przeglądarki",
"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": "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_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",
"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",
"sponsor_desc": "Płatne reklamy, płatne rekomendacje i bezpośrednie reklamy",
"selfpromo": "Nieobłożona/Samopromocja",
"selfpromo_desc": "Nieobłożona promocja własnych treści twórcy",
"interaction": "Przypomnienie o interakcji",
"interaction_desc": "Widzowie są proszeni o polubienie, subskrypcję lub śledzenie w mediach społecznościowych",
"intro": "Intro",
"intro_desc": "Wprowadzenie do wideo, które można pominąć",
"outro": "Outro/Karty końcowe",
"outro_desc": "Napisy końcowe lub gdy wideo się kończy",
"preview": "Podgląd/Podsumowanie",
"preview_desc": "Krótkie podsumowanie poprzednich wideo lub podgląd nadchodzącej treści",
"music_offtopic": "Sekcja niemuzyczna",
"music_offtopic_desc": "Tylko dla wideo muzycznych. Oznacza sekcje niemuzyczne",
"filler": "Wypełniacz poboczny",
"filler_desc": "Sceny poboczne dodane tylko jako wypełniacz lub dla humoru"
},
"video_info": {
"channel": "Kanał",
"views": "👁 Wyświetlenia",
"likes": "👍 Polubienia",
"upload_date": "📅 Data przesłania",
"duration": "⏱ Czas trwania",
"unknown_channel": "Nieznany kanał",
"unknown_date": "Nieznana data",
"unknown_title": "Nieznany tytuł"
},
"command": {
"running": "Uruchamianie...",
"run_command": "Uruchom polecenie"
},
"selection": {
"none_selected": "0 wybrano",
"one_selected": "1 kategoria wybrana",
"count_selected": "{count} wybrano"
},
"status": {
"ready": "Gotowy",
"file_exists": "⚠️ Plik już istnieje",
"video_file_exists": "⚠️ Plik wideo już istnieje",
"audio_file_exists": "⚠️ Plik audio już istnieje",
"subtitle_file_exists": "⚠️ Plik napisów już istnieje",
"cancelling": "Anulowanie pobierania...",
"thumbnail_saved": "✅ Miniatura zapisana: {filename}",
"thumbnail_error": "❌ Błąd miniatury: {error}",
"thumbnail_no_image": "Brak miniatury do zapisania"
},
"errors": {
"playlist_no_videos": "Błąd: Playlista nie zawiera prawidłowych wideo.",
"playlist_no_url": "Błąd: Nie można pobrać URL pierwszego wideo z playlisty.",
"ytdlp_not_found": "Błąd: Plik wykonywalny yt-dlp nie został znaleziony. Proszę najpierw zainstalować yt-dlp.",
"ytdlp_not_found_path": "Błąd: Plik wykonywalny yt-dlp nie został znaleziony. Może to być spowodowane niepoprawną instalacją lub problemem z PATH.",
"no_data_returned": "Błąd: Brak danych zwróconych z yt-dlp",
"no_format_info": "Błąd: Brak dostępnych informacji o formacie.",
"analysis_timeout": "Błąd: Limit czasu analizy. Spróbuj ponownie.",
"invalid_speed_limit": "❌ Błąd: Nieprawidłowa wartość limitu prędkości ustawiona w ustawieniach.",
"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}",
"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}",
"private_video": "Ten film może być prywatny. Proszę użyć plików cookie z opcji niestandardowych."
},
"update_dialog": {
"title": "Dostępna aktualizacja",
"new_version_available": "Dostępna jest nowa wersja YTSage!",
"current_version_label": "Aktualna wersja:",
"latest_version_label": "Najnowsza wersja:",
"changelog": "Lista zmian",
"download_update": "Pobierz aktualizację",
"remind_later": "Przypomnij później"
},
"playlist": {
"unknown": "Nieznana playlista",
"total_videos": "Wszystkich wideo: {count}",
"display_format": "Playlista: {title} | {count} wideo",
"select_videos_title": "Wybierz Filmy z Playlisty",
"save_as": "Zapisz playlistę jako",
"save_success_title": "Sukces",
"saved_successfully": "Playlista zapisana pomyślnie.",
"save_error_title": "Błąd zapisu",
"no_videos_to_save": "Nie zebrano żadnych wpisów z playlisty!",
"save_error_msg": "Nie udało się zapisać playlisty."
},
"subtitle_selection": {
"count_selected": "{count} wybrano"
},
"file_exists_dialog": {
"title": "Plik już istnieje",
"message": "Plik już istnieje:\n{filename}",
"info": "Ten film został już pobrany."
},
"auto_update": {
"last_check_never": "Ostatnie sprawdzenie: Nigdy",
"last_check": "Ostatnie sprawdzenie: {time}",
"next_check_disabled": "Następne sprawdzenie: Wyłączone",
"next_check_startup": "Następne sprawdzenie: Przy starcie",
"next_check_overdue": "Następne sprawdzenie: Teraz (zaległe)",
"next_check": "Następne sprawdzenie: {time}",
"next_check_error": "Następne sprawdzenie: Błąd obliczania",
"checking": "🔄 Sprawdzanie...",
"check_now": "🔍 Sprawdź aktualizacje teraz",
"current_version": "Bieżąca wersja yt-dlp: {version}"
},
"url_validation": {
"empty_url": "URL nie może być pusty",
"invalid_format": "Nieprawidłowy format URL",
"invalid_scheme": "URL musi zaczynać się od http:// lub https://",
"missing_domain": "Nieprawidłowy URL: brak nazwy domeny",
"unsupported_platform": "YTSage obsługuje tylko adresy URL YouTube i YouTube Music.\nDomena '{domain}' nie jest obsługiwana.",
"invalid_youtu_be": "Nieprawidłowy URL youtu.be: brak ID wideo"
},
"ytdlp_errors": {
"private_video": "To jest prywatny film. Możesz go pobrać, logując się na swoje konto za pomocą plików cookie.\nPrzejdź do 'Opcje Niestandardowe' → 'Zaloguj się z plikami cookie' → 'Wyodrębnij pliki cookie z przeglądarki', aby się uwierzytelnić.",
"age_restricted": "Ten film ma ograniczenie wiekowe. Musisz się zalogować, aby uzyskać do niego dostęp.\nUżyj 'Opcje Niestandardowe' → 'Zaloguj się z plikami cookie', aby uwierzytelnić się swoim kontem.",
"geo_blocked": "Ten film nie jest dostępny w Twoim regionie (geoblokada).\nMożesz potrzebować VPN lub film może być ograniczony w Twoim kraju.",
"video_unavailable": "Ten film został usunięty lub nie jest już dostępny.\nFilm mógł zostać usunięty przez osobę przesyłającą lub usunięty z powodu naruszenia zasad.",
"live_stream": "To jest transmisja na żywo, której nie można pobrać, gdy jest aktywna.\nPoczekaj, aż transmisja się zakończy, a następnie spróbuj pobrać zarchiwizowaną wersję.",
"playlist_error": "Nie można uzyskać dostępu do tej playlisty. Może być prywatna, usunięta lub pusta.\nSprawdź, czy playlista istnieje i jest publicznie dostępna.",
"network_error": "Błąd połączenia sieciowego. Sprawdź swoje połączenie internetowe i spróbuj ponownie.\nJeśli problem będzie się utrzymywał, serwer wideo może być tymczasowo niedostępny.",
"invalid_url": "Nieprawidłowy lub nieobsługiwany URL. Sprawdź link i spróbuj ponownie.\nUpewnij się, że używasz prawidłowego adresu URL YouTube, Vimeo lub innej obsługiwanej platformy.",
"premium_content": "Ta treść wymaga YouTube Premium lub członkostwa w kanale.\nMusisz zalogować się na konto, które ma dostęp do tej treści.",
"copyright_blocked": "Ten film jest zablokowany z powodu roszczeń dotyczących praw autorskich.\nWłaściciel treści ograniczył dostęp do tego filmu.",
"extraction_failed": "Nie udało się wyodrębnić informacji o filmie. To może być tymczasowy problem.\nSpróbuj ponownie za kilka minut lub sprawdź, czy link do filmu jest poprawny.",
"generic_error": "Nie udało się wyodrębnić informacji o filmie. Sprawdź swój link.\nSzczegóły techniczne: {error}"
},
"history": {
"title": "Historia Pobierania",
"clear_all": "Wyczyść Wszystko",
"clear_confirm_title": "Wyczyścić Historię?",
"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",
"remove": "Usuń",
"file_not_found": "Nie znaleziono pliku",
"file_not_found_message": "Plik został przeniesiony lub usunięty:\n{path}",
"downloaded_on": "Pobrano: {date}",
"file_size": "Rozmiar: {size}",
"audio_download": "Audio",
"video_download": "Wideo",
"remove_confirm_title": "Usunąć?",
"remove_confirm_message": "Usunąć z historii?\n\n{title}",
"item_removed": "Usunięto",
"history_cleared": "Historia wyczyszczona",
"entries_count": "{count} pobrań",
"one_entry": "1 pobranie",
"redownload_confirm_title": "Pobrać Ponownie?",
"redownload_confirm_message": "Pobrać ponownie?\n\n{title}",
"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",
"current_version": "Obecna wersja:",
"latest_version": "Najnowsza wersja:",
"status_up_to_date": "✓ Aktualne",
"status_update_available": "⚠ Dostępna aktualizacja",
"status_not_installed": "✗ Nie zainstalowano",
"status_idle": "Kliknij 'Sprawdź Wersję', aby rozpocząć",
"status_checking": "🔄 Sprawdzanie wersji...",
"check_updates": "Sprawdź Wersję",
"check_failed": "Nie udało się sprawdzić wersji. Sprawdź połączenie internetowe.",
"description": "Sprawdź swoją wersję FFmpeg i porównaj ją z najnowszą dostępną wersją.",
"guide_info": " Aby zainstalować lub zaktualizować FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>kliknij tutaj, aby wyświetlić nasz kompleksowy przewodnik instalacji</a>."
},
"deno": {
"setup_required": "Wymagana konfiguracja Deno",
"setup_description": "YTSage wymaga Deno do uruchomienia niektórych funkcji.<br><br>Deno nie zostało znalezione w lokalnym katalogu aplikacji. YTSage musi skonfigurować Deno dla Twojego systemu {os_name}.<br><br>Kliknij 'Skonfiguruj Deno', aby pobrać i zainstalować automatycznie.",
"setup_button": "Skonfiguruj Deno",
"downloading": "Pobieranie Deno...",
"extracting": "Wyodrębnianie Deno...",
"verifying": "Weryfikacja Deno...",
"success": "Deno zostało pomyślnie zainstalowane!",
"download_failed": "Pobieranie nie powiodło się",
"download_error": "Nie udało się pobrać Deno: {error}",
"verification_failed": "Weryfikacja SHA256 nie powiodła się. Pobrany plik może być uszkodzony lub zmodyfikowany.",
"setup_failed": "Nie udało się skonfigurować Deno. Niektóre funkcje mogą nie działać poprawnie.",
"setup_error": "Błąd konfiguracji"
},
"deno_updater": {
"title": "Sprawdzanie i Aktualizacja Wersji Deno",
"description": "Sprawdź swoją wersję Deno i zaktualizuj do najnowszej wersji.",
"current_version": "Aktualna Wersja:",
"latest_version": "Najnowsza Wersja:",
"status_idle": "Kliknij 'Sprawdź Aktualizacje', aby zacząć",
"status_checking": "🔄 Sprawdzanie aktualizacji...",
"status_up_to_date": "✓ Zaktualizowane",
"status_update_available": "⚠ Aktualizacja dostępna",
"status_not_installed": "✗ Nie zainstalowano",
"check_updates": "Sprawdź Aktualizacje",
"update_now": "Zaktualizuj Deno",
"updating": "🔄 Aktualizowanie Deno...",
"update_success": "✅ Deno zostało pomyślnie zaktualizowane!",
"update_failed": "❌ Aktualizacja nie powiodła się: {error}",
"check_failed": "Nie udało się sprawdzić aktualizacji. Sprawdź połączenie internetowe."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Português (Portuguese)",
"select_language": "Selecionar idioma:",
"current_language": "Idioma atual: {language}",
"restart_required": "As alterações de idioma terão efeito após reiniciar a aplicação.",
"english": "Inglês",
"spanish": "Espanhol",
"portuguese": "Português",
"russian": "Русский (Russian)",
"chinese": "中文 (简体) (Chinese Simplified)",
"german": "Alemão",
"french": "Francês",
"hindi": "Hindi",
"indonesian": "Indonésio",
"turkish": "Turco",
"polish": "Polonês",
"italian": "Italiano",
"arabic": "Árabe",
"japanese": "Japonês",
"help_text": "Selecione seu idioma preferido para a interface.",
"restart_notice": "A alteração de idioma terá efeito após reiniciar a aplicação."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Pronto"
},
"formats": {
"show_formats": "Mostrar Formatos:",
"video_format": "Formato de Vídeo:",
"audio_format": "Formato de Áudio:",
"no_formats": "Nenhum formato disponível",
"loading": "Carregando formatos...",
"select": "Selecionar",
"quality": "Qualidade",
"extension": "Extensão",
"resolution": "Resolução",
"file_size": "Tamanho do Arquivo",
"codec": "Codec",
"audio": "Áudio",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "Irá mesclar áudio",
"has_audio": "✓ Tem Áudio",
"audio_only": "Apenas Áudio",
"best_4k": "Melhor (4K)",
"best_2k": "Melhor (2K)",
"high_1080p": "Alta (1080p)",
"high_720p": "Alta (720p)",
"medium_480p": "Média (480p)",
"low_quality": "Baixa Qualidade",
"best_audio": "Melhor Áudio",
"high_audio": "Áudio Alto",
"medium_audio": "Áudio Médio",
"low_audio": "Áudio Baixo",
"audio_only_resolution": "Apenas áudio"
},
"buttons": {
"reset": "Redefinir",
"download": "Baixar",
"pause": "Pausar",
"resume": "Retomar",
"cancel": "Cancelar",
"browse": "Procurar",
"clear": "Limpar",
"ok": "OK",
"apply": "Aplicar",
"close": "Fechar",
"run_command": "Executar Comando",
"custom_command_help": "Ajuda",
"about": "Sobre",
"analyze": "Analisar",
"paste_url": "Colar URL",
"select_videos": "Selecionar Vídeos...",
"video": "Vídeo",
"audio_only": "Apenas Áudio",
"custom_options": "Opções Personalizadas",
"trim_video": "Cortar Vídeo",
"download_settings": "Configurações de Download",
"update": "Atualizar yt-dlp",
"select_defaults": "Selecionar Padrões",
"select_all": "Selecionar Tudo",
"deselect_all": "Desmarcar Tudo",
"open_folder": "Abrir local da pasta",
"history": "Histórico",
"save_playlist": "Salvar playlist como"
},
"dialogs": {
"custom_options": "Opções Personalizadas",
"settings": "Configurações",
"select_folder": "Selecionar Pasta de Download",
"sponsorblock_categories": "Categorias SponsorBlock",
"sponsorblock_description": "Selecione quais tipos de segmentos de vídeo serão removidos automaticamente durante o download.\nO SponsorBlock usa dados enviados pela comunidade para identificar esses segmentos.",
"select_subtitles": "Selecionar Legendas",
"filter_languages_placeholder": "Filtrar idiomas (ex., en, pt)...",
"no_subtitles_available": "Nenhuma legenda disponível",
"matching": "correspondendo",
"ytdlp_log_title": "Log do yt-dlp",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Entrar com Cookies",
"custom_command": "Comando Personalizado",
"proxy": "Proxy",
"language": "Idioma",
"updater": "Atualizador"
},
"cookies": {
"help_text": "Escolha como fornecer cookies para fazer login.\nIsso permite baixar vídeos privados e áudio de qualidade premium.",
"cookie_source": "Fonte de Cookies",
"use_cookie_file": "Usar arquivo de cookies",
"extract_from_browser": "Extrair do navegador",
"recommended": "Recomendado",
"cookie_file": "Arquivo de Cookies",
"cookie_file_placeholder": "Caminho para o arquivo cookies.txt...",
"browser_selection": "Seleção de Navegador",
"browser_help": "Selecione o navegador do qual extrair cookies:",
"browser_label": "Navegador:",
"profile_label": "Perfil:",
"profile_placeholder": "padrão",
"browser_extract_message": "Os cookies do navegador serão extraídos quando aplicados",
"file_selected_message": "Arquivo de cookies selecionado - Clique em OK para aplicar",
"select_file_title": "Selecionar Arquivo de Cookies",
"file_filter": "Arquivos de cookies (*.txt *.lwp)",
"file_selected_title": "Arquivo de Cookies Aplicado",
"file_applied_message": "Arquivo de cookies aplicado: {path}",
"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",
"active_browser": "✓ Ativos: cookies do navegador ({browser})",
"active_file": "✓ Ativo: arquivo de cookies ({file})",
"none_active": "○ Nenhum cookie ativo",
"remember_settings": "Lembrar configurações de cookies na próxima inicialização"
},
"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.",
"input_label": "Argumentos yt-dlp:",
"input_placeholder": "Digite os argumentos yt-dlp aqui...\n\nex. --extract-audio --audio-format mp3",
"output_label": "Saída do Comando:",
"output_placeholder": "A saída do comando aparecerá aqui...",
"command_placeholder": "Digite comando yt-dlp personalizado...",
"command_help": "Marcadores disponíveis:\n{url} - URL do Vídeo\n{output} - Diretório de saída\n\nExemplo: --write-info-json --write-thumbnail",
"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}",
"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.",
"main_proxy": "Proxy Principal",
"main_proxy_help": "Servidor proxy principal para todos os downloads. Suporta protocolos HTTP/HTTPS e SOCKS5.",
"proxy_url_label": "URL do Proxy:",
"proxy_url_placeholder": "http://proxy:porta ou socks5://proxy:porta",
"proxy_examples": "Exemplos:\n• HTTP: http://proxy.exemplo.com:8080\n• SOCKS5: socks5://proxy.exemplo.com:1080",
"geo_proxy": "Proxy de Contorno Geográfico",
"geo_proxy_help": "Proxy secundário especificamente para contornar restrições geográficas.",
"geo_proxy_url_label": "URL do Proxy de Contorno Geográfico:",
"geo_proxy_url_placeholder": "http://geo-proxy:porta",
"clear_main_proxy": "Limpar Proxy Principal",
"clear_geo_proxy": "Limpar Proxy Geográfico",
"proxy_url": "URL do Proxy",
"proxy_placeholder": "http://proxy:porta ou socks5://proxy:porta",
"geo_bypass": "Proxy de contorno geográfico (para restrições geográficas)",
"geo_bypass_placeholder": "http://proxy:porta para contornar bloqueios geográficos",
"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",
"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...",
"starting": "🚀 Iniciando download...",
"fetching_info": "🔍 Buscando informações do vídeo...",
"preparing_streams": "🎯 Preparando streams de vídeo...",
"downloading_audio": "⏬ Baixando áudio...",
"downloading_video": "⏬ Baixando vídeo...",
"downloading_subtitle": "⏬ Baixando legendas...",
"downloading": "⏬ Baixando...",
"completed": "✅ Download concluído!",
"video_completed": "✅ Download de vídeo concluído!",
"audio_completed": "✅ Download de áudio concluído!",
"subtitle_completed": "✅ Download de legendas concluído!",
"completed_cleaning": "✅ Download concluído! Limpando...",
"cancelled": "Download cancelado",
"processing_playlist": "📋 Processando dados da playlist...",
"paused": "Download pausado",
"resumed": "Download retomado",
"merging_formats": "✨ Pós-processamento: Mesclando formatos...",
"removing_sponsor_segments": "✨ Pós-processamento: Removendo segmentos patrocinados...",
"speed": "Velocidade",
"eta": "Tempo restante",
"please_enter_url": "Por favor, digite uma URL primeiro.",
"please_set_path": "Por favor, defina um caminho de download primeiro.",
"please_enter_url_and_path": "Por favor, digite uma URL e defina o caminho de download.",
"please_select_format": "Por favor, selecione um formato primeiro.",
"downloading_fallback": "⚡ Baixando..."
},
"update": {
"title": "Atualizar yt-dlp",
"checking": "Verificando atualizações...",
"update_available": "Atualização disponível!\nVersão atual: {current}\nÚltima versão: {latest}",
"up_to_date": "yt-dlp está atualizado (versão {version})",
"could_not_determine": "Não foi possível determinar as versões.",
"error_comparing": "Erro ao comparar versões: {error}",
"update_available_failed": "Atualização disponível! (Comparação falhou)\nAtual: {current}\nÚltima: {latest}",
"updating": "Atualizando...",
"initializing": "🚀 Inicializando processo de atualização...",
"checking_current": "🔍 Verificando instalação atual...",
"found_at": "📍 yt-dlp encontrado em: {path}",
"error_getting_path": "❌ Erro ao obter caminho do yt-dlp: {error}",
"updating_binary": "📦 Atualizando binário yt-dlp gerenciado pela aplicação...",
"update_failed": "❌ Falha ao atualizar yt-dlp. Tente novamente ou verifique sua conexão com a internet.",
"binary_updated": "✅ Binário atualizado com sucesso!",
"update_failed_stderr": "❌ Atualização do yt-dlp falhou: {error}",
"update_timeout": "❌ Atualização do yt-dlp expirou.",
"unexpected_error": "❌ Erro inesperado durante atualização: {error}",
"already_up_to_date": "✅ yt-dlp já está atualizado!",
"update_success": "✅ yt-dlp foi atualizado com sucesso!",
"already_latest": "yt-dlp está atualizado (versão {version})",
"network_error": "❌ Erro de rede durante a atualização: {error}",
"general_error": "❌ A atualização falhou: {error}",
"update_in_progress_title": "Atualização em andamento",
"update_in_progress_message": "O yt-dlp está sendo atualizado. Por favor, aguarde um momento."
},
"about": {
"title": "Sobre YTSage",
"version": "Versão {version}",
"description": "Downloader moderno do YouTube com interface limpa em PySide6.",
"author": "Por: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Detectado",
"missing": "✗ Ausente",
"not_available": "Não Disponível"
},
"time_range": {
"title": "Cortar Vídeo",
"time_range_group": "Intervalo de Tempo",
"start_time": "Tempo de Início (HH:MM:SS)",
"end_time": "Tempo de Fim (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Tempo de início (HH:MM:SS)",
"end_time_placeholder": "Tempo de fim (HH:MM:SS)",
"force_keyframes": "Forçar quadros-chave nos pontos de corte",
"help_text": "Defina os tempos de início e fim para cortar o vídeo.\nDeixe vazio para baixar o vídeo completo.",
"invalid_format": "Formato de tempo inválido. Use o formato HH:MM:SS.",
"start_after_end": "O tempo de início não pode ser após o tempo de fim."
},
"settings": {
"title": "Configurações de Download",
"download_path": "Caminho de Download",
"browse": "Procurar...",
"speed_limit": "Limite de Velocidade",
"speed_limit_placeholder": "Nenhum",
"generic_mode": "Modo genérico",
"enable_generic_mode": "Ativar modo genérico (suporte a sites que não são do YouTube)",
"generic_mode_help": "Permite baixar de Dailymotion, CBC Gem e outros sites compatíveis com o yt-dlp.",
"auto_update_ytdlp": "Auto-Atualizar yt-dlp",
"enable_auto_updates": "Habilitar atualizações automáticas do yt-dlp",
"update_frequency": "Frequência de atualização:",
"check_startup": "Verificar a cada inicialização (mínimo 1 hora entre verificações)",
"check_daily": "Verificar diariamente",
"check_weekly": "Verificar semanalmente",
"check_updates_now": "Atualizar yt-dlp",
"update_check_title": "Verificação de Atualização",
"could_not_determine_version": "Não foi possível determinar a versão atual do yt-dlp.",
"update_available_dialog": "Atualização disponível!\n\nAtual: {current}\nÚltima: {latest}\n\nClique em OK para continuar com a atualização.",
"up_to_date_dialog": "yt-dlp está atualizado!\n\nVersão atual: {version}",
"error_checking_updates": "Erro ao verificar atualizações: {error}",
"settings_saved_title": "Configurações Salvas",
"settings_saved_message": "Configurações de auto-atualização foram salvas com sucesso!",
"error_title": "Erro",
"failed_save_settings": "Falha ao salvar configurações de auto-atualização.",
"error_saving_settings": "Erro ao salvar configurações de auto-atualização: {error}",
"ytdlp_channel": "Canal de Lançamento do yt-dlp",
"ytdlp_channel_stable": "Estável (Lançamentos testados)",
"ytdlp_channel_nightly": "Nightly (Atualizações diárias, recomendado pelo yt-dlp)",
"ytdlp_channel_description": "Escolha entre lançamentos estáveis e nightly. Builds nightly são atualizadas diariamente com as últimas correções e recursos. Você pode trocar de canal a qualquer momento.",
"ytdlp_switching_channel": "Mudando para o canal {channel}...",
"ytdlp_channel_switched": "✅ Mudado com sucesso para o canal {channel}!",
"ytdlp_channel_switch_failed": "❌ Falha ao trocar de canal: {error}",
"ytdlp_current_channel": "Canal atual: {channel}",
"app_updates_title": "Atualizações do YTSage",
"check_app_updates": "Verificar atualizações do YTSage na inicialização",
"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.",
"current_status": "Status Atual",
"current_version_label": "Versão atual do yt-dlp: Verificando...",
"last_check_label": "Última verificação de atualização: Nunca",
"next_check_label": "Próxima verificação: Baseada nas configurações",
"manual_check_button": "🔍 Verificar Atualizações Agora",
"update_frequency_group": "Frequência de Atualização",
"save_settings": "Salvar Configurações",
"settings_saved_successfully": "✅ Configurações salvas com sucesso!",
"error_saving": "❌ Erro ao salvar configurações: {error}",
"output_format_settings": "Configurações de Formato de Saída",
"force_output_format": "Forçar formato de saída ao mesclar",
"preferred_format": "Formato preferido:",
"force_format_help": "Quando ativado, os vídeos mesclados serão convertidos para o seu formato preferido. Desative para deixar o yt-dlp decidir automaticamente.",
"format_mp4": "MP4 (Mais compatível)",
"format_webm": "WebM (Moderno, aberto)",
"format_mkv": "MKV (Rico em recursos)",
"audio_format_settings": "Configurações de formato de áudio",
"force_audio_format": "Forçar formato de áudio para downloads somente de áudio",
"audio_normalization": "Normalização de Áudio (EBU R128)",
"audio_normalization_help": "Quando ativado, as faixas de áudio serão normalizadas. Nota: isso requer recodificação, portanto, um formato de áudio específico (como MP3 ou M4A) deve ser forçado.",
"preferred_audio_format": "Formato de áudio preferido:",
"force_audio_format_help": "Quando ativado, downloads somente de áudio serão convertidos para seu formato preferido. Isso se aplica apenas ao baixar formatos de áudio.",
"audio_format_best": "Melhor (Sem conversão)",
"audio_format_aac": "AAC (Bom para edição)",
"audio_format_mp3": "MP3 (Universal)",
"audio_format_flac": "FLAC (Sem perdas)",
"audio_format_wav": "WAV (Não comprimido)",
"audio_format_opus": "Opus (Eficiente)",
"audio_format_m4a": "M4A (Apple)",
"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.",
"tab_general": "Geral",
"tab_format": "Formato",
"tab_file": "Arquivo",
"concurrent_fragments": "Conexões simultâneas",
"concurrent_fragments_help": "Número de conexões por download. Valores mais altos contornam o limite de velocidade, mas podem causar bloqueios temporários se forem muito altos. Padrão: 1.",
"defaults_settings": "Configurações de seleção padrão",
"default_video_quality": "Resolução de vídeo padrão (altura):",
"default_subtitle_language": "Idioma(s) de legenda padrão:",
"defaults_help": "Defina sua altura de vídeo preferida e os idiomas das legendas (separados por vírgula). Eles serão selecionados automaticamente se disponíveis."
},
"main_ui": {
"url_placeholder": "Digite a URL do vídeo ou playlist do YouTube",
"url_placeholder_generic": "Digite a URL de um vídeo ou playlist de qualquer site compatível",
"merge_subtitles": "Mesclar Legendas",
"save_thumbnail": "Salvar Miniatura",
"save_description": "Salvar Descrição",
"embed_chapters": "Incorporar Capítulos",
"subtitles_selected": "{count} selecionadas",
"all_selected": "Todas selecionadas",
"select_videos_all": "Selecionar Vídeos... (Todas selecionadas)",
"please_enter_url": "Por favor, digite uma URL primeiro",
"error_title": "Erro",
"cookie_file_selected_title": "Arquivo de Cookies Selecionado",
"cookie_file_selected_message": "Arquivo de cookies selecionado: {path}",
"browser_cookies_selected_title": "Cookies do Navegador Selecionados",
"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": "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_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",
"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",
"sponsor_desc": "Promoção paga, referências pagas e anúncios diretos",
"selfpromo": "Promoção Não Paga/Própria",
"selfpromo_desc": "Promoção não paga do próprio conteúdo dos criadores",
"interaction": "Lembrete de Interação",
"interaction_desc": "Pedindo aos espectadores para curtir, se inscrever ou seguir nas redes sociais",
"intro": "Introdução",
"intro_desc": "Introdução do vídeo que pode ser pulada",
"outro": "Encerramento/Cards Finais",
"outro_desc": "Créditos ou quando o vídeo termina",
"preview": "Prévia/Recapitulação",
"preview_desc": "Recapitulação rápida de vídeos anteriores ou prévia do que está por vir",
"music_offtopic": "Seção Não Musical",
"music_offtopic_desc": "Apenas para vídeos musicais. Marca seções não musicais",
"filler": "Tangente de Preenchimento",
"filler_desc": "Cenas tangenciais adicionadas apenas para preenchimento ou humor"
},
"video_info": {
"channel": "Canal",
"views": "👁 Visualizações",
"likes": "👍 Curtidas",
"upload_date": "📅 Data de envio",
"duration": "⏱ Duração",
"unknown_channel": "Canal desconhecido",
"unknown_date": "Data desconhecida",
"unknown_title": "Título desconhecido"
},
"command": {
"running": "Executando...",
"run_command": "Executar Comando"
},
"selection": {
"none_selected": "0 selecionadas",
"one_selected": "1 categoria selecionada",
"count_selected": "{count} selecionadas"
},
"status": {
"ready": "Pronto",
"file_exists": "⚠️ Arquivo já existe",
"video_file_exists": "⚠️ Arquivo de vídeo já existe",
"audio_file_exists": "⚠️ Arquivo de áudio já existe",
"subtitle_file_exists": "⚠️ Arquivo de legendas já existe",
"cancelling": "Cancelando download...",
"thumbnail_saved": "✅ Miniatura salva: {filename}",
"thumbnail_error": "❌ Erro na miniatura: {error}",
"thumbnail_no_image": "Nenhuma miniatura disponível para salvar"
},
"errors": {
"playlist_no_videos": "Erro: A playlist não contém vídeos válidos.",
"playlist_no_url": "Erro: Não foi possível obter a URL do primeiro vídeo da playlist.",
"ytdlp_not_found": "Erro: Executável yt-dlp não encontrado. Por favor, instale o yt-dlp primeiro.",
"ytdlp_not_found_path": "Erro: Executável yt-dlp não encontrado. Isso pode ser devido a uma instalação inadequada ou problema de PATH.",
"no_data_returned": "Erro: Nenhum dado retornado do yt-dlp",
"no_format_info": "Erro: Nenhuma informação de formato disponível.",
"analysis_timeout": "Erro: Tempo de análise esgotado. Por favor, tente novamente.",
"invalid_speed_limit": "❌ Erro: Valor de limite de velocidade inválido definido nas configurações.",
"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}",
"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}",
"private_video": "Este vídeo pode ser privado. Por favor, use cookies das opções personalizadas."
},
"update_dialog": {
"title": "Atualização Disponível",
"new_version_available": "Uma nova versão do YTSage está disponível!",
"current_version_label": "Versão atual:",
"latest_version_label": "Última versão:",
"changelog": "Registro de alterações",
"download_update": "Baixar Atualização",
"remind_later": "Lembrar Mais Tarde"
},
"playlist": {
"unknown": "Playlist Desconhecida",
"total_videos": "Total de Vídeos: {count}",
"display_format": "Playlist: {title} | {count} vídeos",
"select_videos_title": "Selecionar Vídeos da Playlist",
"save_as": "Salvar playlist como",
"save_success_title": "Sucesso",
"saved_successfully": "Playlist salva com sucesso.",
"save_error_title": "Erro ao salvar",
"no_videos_to_save": "Nenhuma entrada de playlist coletada!",
"save_error_msg": "Falha ao salvar a playlist."
},
"subtitle_selection": {
"count_selected": "{count} selecionadas"
},
"file_exists_dialog": {
"title": "Arquivo já existe",
"message": "O arquivo já existe:\n{filename}",
"info": "Este vídeo já foi baixado."
},
"auto_update": {
"last_check_never": "Última verificação: Nunca",
"last_check": "Última verificação: {time}",
"next_check_disabled": "Próxima verificação: Desativada",
"next_check_startup": "Próxima verificação: Na inicialização",
"next_check_overdue": "Próxima verificação: Agora (atrasada)",
"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",
"current_version": "Versão atual do yt-dlp: {version}"
},
"url_validation": {
"empty_url": "O URL não pode estar vazio",
"invalid_format": "Formato de URL inválido",
"invalid_scheme": "O URL deve começar com http:// ou https://",
"missing_domain": "URL inválido: nome de domínio ausente",
"unsupported_platform": "YTSage suporta apenas URLs do YouTube e YouTube Music.\nO domínio '{domain}' não é suportado.",
"invalid_youtu_be": "URL youtu.be inválido: ID do vídeo ausente"
},
"ytdlp_errors": {
"private_video": "Este é um vídeo privado. Você pode baixá-lo fazendo login em sua conta usando cookies.\nVá para 'Opções Personalizadas' → 'Login com Cookies' → 'Extrair cookies do navegador' para autenticar.",
"age_restricted": "Este vídeo tem restrição de idade. Você precisa estar conectado para acessá-lo.\nUse 'Opções Personalizadas' → 'Login com Cookies' para autenticar com sua conta.",
"geo_blocked": "Este vídeo não está disponível em sua região (geo-bloqueado).\nVocê pode precisar usar uma VPN ou o vídeo pode estar restrito em seu país.",
"video_unavailable": "Este vídeo foi removido ou não está mais disponível.\nO vídeo pode ter sido excluído pelo uploader ou removido devido a violações de políticas.",
"live_stream": "Esta é uma transmissão ao vivo que não pode ser baixada enquanto está ativa.\nAguarde o término da transmissão e tente baixar a versão arquivada.",
"playlist_error": "Não é possível acessar esta playlist. Ela pode ser privada, excluída ou vazia.\nVerifique se a playlist existe e está acessível publicamente.",
"network_error": "Erro de conexão de rede. Verifique sua conexão com a internet e tente novamente.\nSe o problema persistir, o servidor de vídeo pode estar temporariamente indisponível.",
"invalid_url": "URL inválido ou não suportado. Verifique o link e tente novamente.\nCertifique-se de usar um URL válido do YouTube, Vimeo ou outra plataforma suportada.",
"premium_content": "Este conteúdo requer YouTube Premium ou associação ao canal.\nVocê precisa fazer login com uma conta que tenha acesso a este conteúdo.",
"copyright_blocked": "Este vídeo está bloqueado devido a reivindicações de direitos autorais.\nO proprietário do conteúdo restringiu o acesso a este vídeo.",
"extraction_failed": "Falha ao extrair informações do vídeo. Isso pode ser um problema temporário.\nTente novamente em alguns minutos ou verifique se o link do vídeo está correto.",
"generic_error": "Não foi possível extrair informações do vídeo. Verifique seu link.\nDetalhes técnicos: {error}"
},
"history": {
"title": "Histórico de Downloads",
"clear_all": "Limpar Tudo",
"clear_confirm_title": "Limpar Histórico?",
"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",
"remove": "Remover",
"file_not_found": "Arquivo não encontrado",
"file_not_found_message": "O arquivo foi movido ou excluído:\n{path}",
"downloaded_on": "Baixado: {date}",
"file_size": "Tamanho: {size}",
"audio_download": "Áudio",
"video_download": "Vídeo",
"remove_confirm_title": "Remover?",
"remove_confirm_message": "Remover do histórico?\n\n{title}",
"item_removed": "Removido",
"history_cleared": "Histórico limpo",
"entries_count": "{count} downloads",
"one_entry": "1 download",
"redownload_confirm_title": "Baixar Novamente?",
"redownload_confirm_message": "Baixar novamente?\n\n{title}",
"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",
"current_version": "Versão Atual:",
"latest_version": "Última Versão:",
"status_up_to_date": "✓ Atualizado",
"status_update_available": "⚠ Atualização disponível",
"status_not_installed": "✗ Não instalado",
"status_idle": "Clique em 'Verificar Versão' para começar",
"status_checking": "🔄 Verificando versão...",
"check_updates": "Verificar Versão",
"check_failed": "Falha ao verificar versão. Verifique sua conexão com a Internet.",
"description": "Verifique sua versão do FFmpeg e compare com a última versão disponível.",
"guide_info": " Para instalar ou atualizar o FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>clique aqui para ver nosso guia completo de instalação</a>."
},
"deno": {
"setup_required": "Configuração do Deno Necessária",
"setup_description": "O YTSage requer o Deno para executar determinados recursos.<br><br>O Deno não foi encontrado no diretório local do aplicativo. O YTSage precisa configurar o Deno para o seu sistema {os_name}.<br><br>Clique em 'Configurar Deno' para baixar e instalar automaticamente.",
"setup_button": "Configurar Deno",
"downloading": "Baixando Deno...",
"extracting": "Extraindo Deno...",
"verifying": "Verificando Deno...",
"success": "Deno foi instalado com sucesso!",
"download_failed": "Falha no Download",
"download_error": "Falha ao baixar o Deno: {error}",
"verification_failed": "Falha na verificação SHA256. O arquivo baixado pode estar corrompido ou adulterado.",
"setup_failed": "Falha ao configurar o Deno. Alguns recursos podem não funcionar corretamente.",
"setup_error": "Erro de Configuração"
},
"deno_updater": {
"title": "Verificador e Atualizador de Versão do Deno",
"description": "Verifique sua versão do Deno e atualize para a versão mais recente.",
"current_version": "Versão Atual:",
"latest_version": "Última Versão:",
"status_idle": "Clique em 'Verificar Atualizações' para começar",
"status_checking": "🔄 Verificando atualizações...",
"status_up_to_date": "✓ Atualizado",
"status_update_available": "⚠ Atualização disponível",
"status_not_installed": "✗ Não instalado",
"check_updates": "Verificar Atualizações",
"update_now": "Atualizar Deno",
"updating": "🔄 Atualizando Deno...",
"update_success": "✅ Deno foi atualizado com sucesso!",
"update_failed": "❌ Falha na atualização: {error}",
"check_failed": "Falha ao verificar atualizações. Verifique sua conexão com a Internet."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Русский (Russian)",
"select_language": "Выберите язык:",
"current_language": "Текущий язык: {language}",
"restart_required": "Изменения языка вступят в силу после перезапуска приложения.",
"english": "Английский",
"spanish": "Испанский",
"portuguese": "Португальский",
"russian": "Русский",
"chinese": "中文 (简体) (Chinese Simplified)",
"german": "Немецкий",
"french": "Французский",
"hindi": "Хинди",
"indonesian": "Индонезийский",
"turkish": "Турецкий",
"polish": "Польский",
"italian": "Итальянский",
"arabic": "Арабский",
"japanese": "Японский",
"help_text": "Выберите предпочитаемый язык для интерфейса.",
"restart_notice": "Изменение языка вступит в силу после перезапуска приложения."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Готово"
},
"formats": {
"show_formats": "Показать форматы:",
"video_format": "Видео формат:",
"audio_format": "Аудио формат:",
"no_formats": "Форматы недоступны",
"loading": "Загрузка форматов...",
"select": "Выбрать",
"quality": "Качество",
"extension": "Расширение",
"resolution": "Разрешение",
"file_size": "Размер файла",
"codec": "Кодек",
"audio": "Аудио",
"fps": "Кадр/с",
"hdr": "HDR",
"will_merge_audio": "Объединит аудио",
"has_audio": "✓ Есть аудио",
"audio_only": "Только аудио",
"best_4k": "Лучшее (4K)",
"best_2k": "Лучшее (2K)",
"high_1080p": "Высокое (1080p)",
"high_720p": "Высокое (720p)",
"medium_480p": "Среднее (480p)",
"low_quality": "Низкое качество",
"best_audio": "Лучшее аудио",
"high_audio": "Высокое аудио",
"medium_audio": "Среднее аудио",
"low_audio": "Низкое аудио",
"audio_only_resolution": "Только аудио"
},
"buttons": {
"reset": "Сброс",
"download": "Скачать",
"pause": "Пауза",
"resume": "Продолжить",
"cancel": "Отмена",
"browse": "Обзор",
"clear": "Очистить",
"ok": "OK",
"apply": "Применить",
"close": "Закрыть",
"run_command": "Выполнить команду",
"custom_command_help": "Помощь",
"about": "О программе",
"analyze": "Анализировать",
"paste_url": "Вставить URL",
"select_videos": "Выбрать видео...",
"video": "Видео",
"audio_only": "Только аудио",
"custom_options": "Пользовательские опции",
"trim_video": "Обрезать видео",
"download_settings": "Настройки загрузки",
"update": "Обновить yt-dlp",
"select_defaults": "Выбрать по умолчанию",
"select_all": "Выбрать всё",
"deselect_all": "Снять выделение",
"open_folder": "Открыть расположение папки",
"history": "История",
"save_playlist": "Сохранить плейлист как"
},
"dialogs": {
"custom_options": "Пользовательские опции",
"settings": "Настройки",
"select_folder": "Выбрать папку загрузки",
"sponsorblock_categories": "Категории SponsorBlock",
"sponsorblock_description": "Выберите типы сегментов видео, которые будут автоматически удалены во время загрузки.\nSponsorBlock использует данные, отправленные сообществом, для идентификации этих сегментов.",
"select_subtitles": "Выбрать субтитры",
"filter_languages_placeholder": "Фильтр языков (например, en, ru)...",
"no_subtitles_available": "Субтитры недоступны",
"matching": "соответствующие",
"ytdlp_log_title": "Журнал yt-dlp",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Войти через Cookie",
"custom_command": "Пользовательская команда",
"proxy": "Прокси",
"language": "Язык",
"updater": "Обновление"
},
"cookies": {
"help_text": "Выберите способ предоставления cookie для входа в систему.\nЭто позволяет загружать приватные видео и аудио премиум качества.",
"cookie_source": "Источник Cookie",
"use_cookie_file": "Использовать файл cookie",
"extract_from_browser": "Извлечь из браузера",
"recommended": "Рекомендуется",
"cookie_file": "Файл Cookie",
"cookie_file_placeholder": "Путь к файлу cookies.txt...",
"browser_selection": "Выбор браузера",
"browser_help": "Выберите браузер для извлечения cookie:",
"browser_label": "Браузер:",
"profile_label": "Профиль:",
"profile_placeholder": "по умолчанию",
"browser_extract_message": "Cookie браузера будут извлечены при применении",
"file_selected_message": "Файл cookie выбран - Нажмите OK для применения",
"select_file_title": "Выбрать файл Cookie",
"file_filter": "Файлы cookie (*.txt *.lwp)",
"file_selected_title": "Файл Cookie применён",
"file_applied_message": "Файл cookie применён: {path}",
"browser_selected_title": "Cookie браузера применены",
"browser_applied_message": "Cookie браузера будут извлечены из: {browser}",
"cleared_title": "Cookie очищены",
"cleared_message": "Настройки cookie были очищены",
"active_browser": "✓ Активны: cookie браузера ({browser})",
"active_file": "✓ Активен: файл cookie ({file})",
"none_active": "○ Нет активных cookie",
"remember_settings": "Запомнить настройки cookie при следующем запуске"
},
"custom_command": {
"help_text": "Введите пользовательскую команду yt-dlp ниже. Текущий URL будет добавлен автоматически.<br><br>Для полного списка опций и примеров использования, <a href=\"{docs_url}\">нажмите здесь для просмотра официальной документации yt-dlp</a>.<br><br>Примечание: Путь загрузки и шаблон имени файла будут обработаны автоматически.",
"input_label": "Аргументы yt-dlp:",
"input_placeholder": "Введите аргументы yt-dlp здесь...\n\nнапример: --extract-audio --audio-format mp3",
"output_label": "Вывод команды:",
"output_placeholder": "Вывод команды появится здесь...",
"command_placeholder": "Введите пользовательскую команду yt-dlp...",
"command_help": "Доступные заполнители:\n{url} - URL видео\n{output} - Выходной каталог\n\nПример: --write-info-json --write-thumbnail",
"full_command": "🔧 Полная команда: {command}",
"command_success": "✅ Пользовательская команда выполнена успешно!",
"command_failed": "❌ Команда завершилась с кодом ошибки {code}",
"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": "Настройте параметры прокси для загрузки. Оставьте пустым для прямого подключения.",
"main_proxy": "Основной прокси",
"main_proxy_help": "Основной прокси-сервер для всех загрузок. Поддерживает протоколы HTTP/HTTPS и SOCKS5.",
"proxy_url_label": "URL прокси:",
"proxy_url_placeholder": "http://proxy:port или socks5://proxy:port",
"proxy_examples": "Примеры:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Прокси для обхода географических ограничений",
"geo_proxy_help": "Вторичный прокси специально для обхода географических ограничений.",
"geo_proxy_url_label": "URL прокси для обхода гео-ограничений:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Очистить основной прокси",
"clear_geo_proxy": "Очистить гео-прокси",
"proxy_url": "URL прокси",
"proxy_placeholder": "http://proxy:port или socks5://proxy:port",
"geo_bypass": "Прокси для обхода гео-ограничений",
"geo_bypass_placeholder": "http://proxy:port для обхода гео-блокировок",
"invalid_main_url": "Неверный формат URL основного прокси",
"invalid_geo_url": "Неверный формат URL гео-прокси",
"main_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": "Подготовка загрузки...",
"starting": "🚀 Начинается загрузка...",
"fetching_info": "🔍 Получение информации о видео...",
"preparing_streams": "🎯 Подготовка видеопотоков...",
"downloading_audio": "⏬ Загрузка аудио...",
"downloading_video": "⏬ Загрузка видео...",
"downloading_subtitle": "⏬ Загрузка субтитров...",
"downloading": "⏬ Загрузка...",
"completed": "✅ Загрузка завершена!",
"video_completed": "✅ Загрузка видео завершена!",
"audio_completed": "✅ Загрузка аудио завершена!",
"subtitle_completed": "✅ Загрузка субтитров завершена!",
"completed_cleaning": "✅ Загрузка завершена! Очистка...",
"cancelled": "Загрузка отменена",
"processing_playlist": "📋 Обработка данных плейлиста...",
"paused": "Загрузка приостановлена",
"resumed": "Загрузка возобновлена",
"merging_formats": "✨ Постобработка: Слияние форматов...",
"removing_sponsor_segments": "✨ Постобработка: Удаление рекламных сегментов...",
"speed": "Скорость",
"eta": "Осталось",
"please_enter_url": "Пожалуйста, введите URL сначала.",
"please_set_path": "Пожалуйста, установите путь загрузки сначала.",
"please_enter_url_and_path": "Пожалуйста, введите URL и установите путь загрузки.",
"please_select_format": "Пожалуйста, выберите формат сначала.",
"downloading_fallback": "⚡ Загрузка..."
},
"update": {
"title": "Обновить yt-dlp",
"checking": "Проверка обновлений...",
"update_available": "Доступно обновление!\nТекущая версия: {current}\nПоследняя версия: {latest}",
"up_to_date": "yt-dlp актуален (версия {version})",
"could_not_determine": "Не удалось определить версии.",
"error_comparing": "Ошибка сравнения версий: {error}",
"update_available_failed": "Доступно обновление! (Сравнение не удалось)\nТекущая: {current}\nПоследняя: {latest}",
"updating": "Обновление...",
"initializing": "🚀 Инициализация процесса обновления...",
"checking_current": "🔍 Проверка текущей установки...",
"found_at": "📍 yt-dlp найден в: {path}",
"error_getting_path": "❌ Ошибка получения пути yt-dlp: {error}",
"updating_binary": "📦 Обновление бинарного файла yt-dlp, управляемого приложением...",
"update_failed": "❌ Не удалось обновить yt-dlp. Попробуйте еще раз или проверьте подключение к интернету.",
"binary_updated": "✅ Бинарный файл успешно обновлен!",
"update_failed_stderr": "❌ Обновление yt-dlp не удалось: {error}",
"update_timeout": "❌ Время ожидания обновления yt-dlp истекло.",
"unexpected_error": "❌ Неожиданная ошибка во время обновления: {error}",
"already_up_to_date": "✅ yt-dlp уже актуален!",
"update_success": "✅ yt-dlp был успешно обновлен!",
"already_latest": "yt-dlp актуален (версия {version})",
"network_error": "❌ Ошибка сети во время обновления: {error}",
"general_error": "❌ Обновление не удалось: {error}",
"update_in_progress_title": "Идет обновление",
"update_in_progress_message": "yt-dlp обновляется. Пожалуйста, подождите."
},
"about": {
"title": "О YTSage",
"version": "Версия {version}",
"description": "Современный загрузчик YouTube с чистым интерфейсом PySide6.",
"author": "Автор: {author}",
"github": "GitHub: {repo}",
"system_info": "Системная информация",
"loading": "🔄 Загрузка системной информации...",
"refresh": "🔄",
"open_logs": "📂 Логи",
"logs_tooltip": "Открыть папку с логами приложения",
"refreshing": "🔄 Обновление...",
"refresh_failed": "Обновление не удалось",
"refresh_failed_message": "Не удалось обновить информацию о версии.",
"detected": "✓ Обнаружено",
"missing": "✗ Отсутствует",
"not_available": "Недоступно"
},
"time_range": {
"title": "Обрезать видео",
"time_range_group": "Временной диапазон",
"start_time": "Время начала (HH:MM:SS)",
"end_time": "Время окончания (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Время начала (HH:MM:SS)",
"end_time_placeholder": "Время окончания (HH:MM:SS)",
"force_keyframes": "Принудительные ключевые кадры в точках обрезки",
"help_text": "Установите время начала и окончания для обрезки видео.\nОставьте пустым для загрузки полного видео.",
"invalid_format": "Неверный формат времени. Используйте формат HH:MM:SS.",
"start_after_end": "Время начала не может быть после времени окончания."
},
"settings": {
"title": "Настройки загрузки",
"download_path": "Путь загрузки",
"browse": "Обзор...",
"speed_limit": "Ограничение скорости",
"speed_limit_placeholder": "Нет",
"generic_mode": "Универсальный режим",
"enable_generic_mode": "Включить универсальный режим (поддержка сайтов не только YouTube)",
"generic_mode_help": "Позволяет скачивать с Dailymotion, CBC Gem и других сайтов, поддерживаемых yt-dlp.",
"auto_update_ytdlp": "Автообновление yt-dlp",
"enable_auto_updates": "Включить автоматические обновления yt-dlp",
"update_frequency": "Частота обновления:",
"check_startup": "Проверять при каждом запуске (минимум 1 час между проверками)",
"check_daily": "Проверять ежедневно",
"check_weekly": "Проверять еженедельно",
"check_updates_now": "Обновить yt-dlp",
"update_check_title": "Проверка обновлений",
"could_not_determine_version": "Не удалось определить текущую версию yt-dlp.",
"update_available_dialog": "Доступно обновление!\n\nТекущая: {current}\nПоследняя: {latest}\n\nНажмите OK для продолжения обновления.",
"up_to_date_dialog": "yt-dlp актуален!\n\nТекущая версия: {version}",
"error_checking_updates": "Ошибка проверки обновлений: {error}",
"settings_saved_title": "Настройки сохранены",
"settings_saved_message": "Настройки автообновления успешно сохранены!",
"error_title": "Ошибка",
"failed_save_settings": "Не удалось сохранить настройки автообновления.",
"error_saving_settings": "Ошибка сохранения настроек автообновления: {error}",
"ytdlp_channel": "Канал выпуска yt-dlp",
"ytdlp_channel_stable": "Стабильный (Протестированные релизы)",
"ytdlp_channel_nightly": "Nightly (Ежедневные обновления, рекомендовано yt-dlp)",
"ytdlp_channel_description": "Выберите между стабильными и nightly релизами. Nightly сборки обновляются ежедневно с последними исправлениями и функциями. Вы можете переключать каналы в любое время.",
"ytdlp_switching_channel": "Переключение на канал {channel}...",
"ytdlp_channel_switched": "✅ Успешно переключено на канал {channel}!",
"ytdlp_channel_switch_failed": "❌ Не удалось переключить канал: {error}",
"ytdlp_current_channel": "Текущий канал: {channel}",
"app_updates_title": "Обновления YTSage",
"check_app_updates": "Проверять обновления YTSage при запуске",
"check_beta_updates": "Получать бета-обновления",
"auto_update_title": "Настройки автообновления",
"auto_update_header": "🔄 Настройки автообновления",
"auto_update_description": "Настройте автоматические обновления для yt-dlp, чтобы всегда иметь последние функции и исправления ошибок.",
"current_status": "Текущий статус",
"current_version_label": "Текущая версия yt-dlp: Проверка...",
"last_check_label": "Последняя проверка обновлений: Никогда",
"next_check_label": "Следующая проверка: Основана на настройках",
"manual_check_button": "🔍 Проверить обновления сейчас",
"update_frequency_group": "Частота обновления",
"save_settings": "Сохранить настройки",
"settings_saved_successfully": "✅ Настройки успешно сохранены!",
"error_saving": "❌ Ошибка сохранения настроек: {error}",
"output_format_settings": "Настройки выходного формата",
"force_output_format": "Принудительно задать выходной формат при объединении",
"preferred_format": "Предпочтительный формат:",
"force_format_help": "Когда включено, объединённые видео будут конвертированы в ваш предпочтительный формат. Отключите, чтобы yt-dlp решал автоматически.",
"format_mp4": "MP4 (Наиболее совместимый)",
"format_webm": "WebM (Современный, открытый)",
"format_mkv": "MKV (Многофункциональный)",
"audio_format_settings": "Настройки формата аудио",
"force_audio_format": "Принудительный формат аудио для загрузки только аудио",
"audio_normalization": "Нормализация звука (EBU R128)",
"audio_normalization_help": "При включении звуковые дорожки будут нормализованы. Примечание: Это требует перекодирования, поэтому необходимо принудительно задать определенный аудиоформат (например, MP3 или M4A).",
"preferred_audio_format": "Предпочтительный формат аудио:",
"force_audio_format_help": "При включении загрузки только аудио будут конвертированы в ваш предпочтительный формат. Это применяется только при загрузке аудио форматов.",
"audio_format_best": "Лучшее (Без конвертации)",
"audio_format_aac": "AAC (Хорошо для редактирования)",
"audio_format_mp3": "MP3 (Универсальный)",
"audio_format_flac": "FLAC (Без потерь)",
"audio_format_wav": "WAV (Несжатый)",
"audio_format_opus": "Opus (Эффективный)",
"audio_format_m4a": "M4A (Apple)",
"audio_format_vorbis": "Vorbis (Открытый)",
"filename_format": "Формат имени файла",
"filename_format_help": "Доступные переменные: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Поддерживается стандартный синтаксис шаблона вывода yt-dlp.",
"tab_general": "Общие",
"tab_format": "Формат",
"tab_file": "Файл",
"concurrent_fragments": "Одновременные соединения",
"concurrent_fragments_help": "Количество соединений на одну загрузку. Более высокие значения позволяют обойти ограничение скорости, но могут вызвать временную блокировку при слишком высоких значениях. По умолчанию: 1.",
"defaults_settings": "Настройки выбора по умолчанию",
"default_video_quality": "Разрешение видео по умолчанию (высота):",
"default_subtitle_language": "Язык(и) субтитров по умолчанию:",
"defaults_help": "Установите предпочтительную высоту видео и языки субтитров (через запятую). Они будут выбраны автоматически, إذا كان متاحاً."
},
"main_ui": {
"url_placeholder": "Введите URL видео или плейлиста YouTube",
"url_placeholder_generic": "Введите URL видео или плейлиста с любого поддерживаемого сайта",
"merge_subtitles": "Объединить субтитры",
"save_thumbnail": "Сохранить миниатюру",
"save_description": "Сохранить описание",
"embed_chapters": "Встроить главы",
"subtitles_selected": "выбрано: {count}",
"all_selected": "Все выбраны",
"select_videos_all": "Выбрать видео... (Все выбраны)",
"please_enter_url": "Пожалуйста, введите URL сначала",
"error_title": "Ошибка",
"cookie_file_selected_title": "Файл Cookie выбран",
"cookie_file_selected_message": "Файл cookie выбран: {path}",
"browser_cookies_selected_title": "Cookie браузера выбраны",
"browser_cookies_selected_message": "Cookie браузера будут извлечены из: {browser}",
"error_no_format_info": "Ошибка: Информация о формате недоступна.",
"error_extract_info": "Ошибка: Не удалось извлечь базовую информацию о видео. Проверьте вашу ссылку.",
"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_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": "Пожалуйста, сначала выберите субтитры",
"settings_tooltip": "Текущий путь: {path}\nЛимит скорости: {speed_limit}",
"speed_limit_none": "Нет",
"open_folder_error": "Не удалось открыть папку: {error}",
"time_range_set": "Установлен участок: {section}"
},
"sponsorblock": {
"sponsor": "Спонсор",
"sponsor_desc": "Платная реклама, платные рефералы и прямая реклама",
"selfpromo": "Неоплачиваемая/Собственная реклама",
"selfpromo_desc": "Неоплачиваемая реклама собственного контента создателей",
"interaction": "Напоминание о взаимодействии",
"interaction_desc": "Просьба к зрителям поставить лайк, подписаться или подписаться в социальных сетях",
"intro": "Вступление",
"intro_desc": "Вступление к видео, которое можно пропустить",
"outro": "Концовка/Финальные карточки",
"outro_desc": "Титры или когда видео заканчивается",
"preview": "Предварительный просмотр/Краткое изложение",
"preview_desc": "Краткое изложение предыдущих видео или предварительный просмотр предстоящего",
"music_offtopic": "Немузыкальная секция",
"music_offtopic_desc": "Только для музыкальных видео. Отмечает немузыкальные секции",
"filler": "Отступление-заполнитель",
"filler_desc": "Касательные сцены, добавленные только для заполнения или юмора"
},
"video_info": {
"channel": "Канал",
"views": "👁 Просмотры",
"likes": "👍 Лайки",
"upload_date": "📅 Дата загрузки",
"duration": "⏱ Продолжительность",
"unknown_channel": "Неизвестный канал",
"unknown_date": "Неизвестная дата",
"unknown_title": "Неизвестный заголовок"
},
"command": {
"running": "Выполняется...",
"run_command": "Выполнить команду"
},
"selection": {
"none_selected": "0 выбрано",
"one_selected": "1 категория выбрана",
"count_selected": "выбрано: {count}"
},
"status": {
"ready": "Готово",
"file_exists": "⚠️ Файл уже существует",
"video_file_exists": "⚠️ Видеофайл уже существует",
"audio_file_exists": "⚠️ Аудиофайл уже существует",
"subtitle_file_exists": "⚠️ Файл субтитров уже существует",
"cancelling": "Отмена загрузки...",
"thumbnail_saved": "✅ Миниатюра сохранена: {filename}",
"thumbnail_error": "❌ Ошибка миниатюры: {error}",
"thumbnail_no_image": "Нет доступной миниатюры для сохранения"
},
"errors": {
"playlist_no_videos": "Ошибка: Плейлист не содержит действительных видео.",
"playlist_no_url": "Ошибка: Не удалось получить URL для первого видео плейлиста.",
"ytdlp_not_found": "Ошибка: Исполняемый файл yt-dlp не найден. Пожалуйста, сначала установите yt-dlp.",
"ytdlp_not_found_path": "Ошибка: Исполняемый файл yt-dlp не найден. Это может быть связано с неправильной установкой или проблемой PATH.",
"no_data_returned": "Ошибка: Данные от yt-dlp не возвращены",
"no_format_info": "Ошибка: Информация о формате недоступна.",
"analysis_timeout": "Ошибка: Тайм-аут анализа. Пожалуйста, попробуйте снова.",
"invalid_speed_limit": "❌ Ошибка: Неверное значение ограничения скорости в настройках.",
"ytdlp_failed": "Ошибка: yt-dlp завершился с ошибкой: {error}",
"parse_failed": "Ошибка: Не удалось разобрать вывод yt-dlp: {error}",
"analysis_failed": "Ошибка: Анализ не удался: {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}",
"private_video": "Это видео может быть приватным. Пожалуйста, используйте файлы cookie из пользовательских настроек."
},
"update_dialog": {
"title": "Доступно обновление",
"new_version_available": "Доступна новая версия YTSage!",
"current_version_label": "Текущая версия:",
"latest_version_label": "Последняя версия:",
"changelog": "Список изменений",
"download_update": "Скачать обновление",
"remind_later": "Напомнить позже"
},
"playlist": {
"unknown": "Неизвестный плейлист",
"total_videos": "Всего видео: {count}",
"display_format": "Плейлист: {title} | {count} видео",
"select_videos_title": "Выбрать Видео из Плейлиста",
"save_as": "Сохранить плейлист как",
"save_success_title": "Успех",
"saved_successfully": "Плейлист успешно сохранен.",
"save_error_title": "Ошибка сохранения",
"no_videos_to_save": "Записи плейлиста не собраны!",
"save_error_msg": "Не удалось сохранить плейлист."
},
"subtitle_selection": {
"count_selected": "выбрано: {count}"
},
"file_exists_dialog": {
"title": "Файл уже существует",
"message": "Файл уже существует:\n{filename}",
"info": "Это видео уже было загружено."
},
"auto_update": {
"last_check_never": "Последняя проверка: Никогда",
"last_check": "Последняя проверка: {time}",
"next_check_disabled": "Следующая проверка: Отключена",
"next_check_startup": "Следующая проверка: При запуске",
"next_check_overdue": "Следующая проверка: Сейчас (просрочено)",
"next_check": "Следующая проверка: {time}",
"next_check_error": "Следующая проверка: Ошибка расчета",
"checking": "🔄 Проверка...",
"check_now": "🔍 Проверить обновления сейчас",
"current_version": "Текущая версия yt-dlp: {version}"
},
"url_validation": {
"empty_url": "URL не может быть пустым",
"invalid_format": "Недопустимый формат URL",
"invalid_scheme": "URL должен начинаться с http:// или https://",
"missing_domain": "Недопустимый URL: отсутствует доменное имя",
"unsupported_platform": "YTSage поддерживает только URL-адреса YouTube и YouTube Music.\nДомен '{domain}' не поддерживается.",
"invalid_youtu_be": "Недопустимый URL youtu.be: отсутствует ID видео"
},
"ytdlp_errors": {
"private_video": "Это частное видео. Вы можете загрузить его, войдя в свою учетную запись с помощью cookies.\nПерейдите в 'Пользовательские параметры' → 'Вход с помощью Cookies' → 'Извлечь cookies из браузера' для аутентификации.",
"age_restricted": "Это видео имеет возрастные ограничения. Для доступа необходимо войти в систему.\nИспользуйте 'Пользовательские параметры' → 'Вход с помощью Cookies' для аутентификации с вашей учетной записью.",
"geo_blocked": "Это видео недоступно в вашем регионе (геоблокировка).\nВам может понадобиться VPN или видео может быть ограничено в вашей стране.",
"video_unavailable": "Это видео было удалено или больше не доступно.\nВидео могло быть удалено загрузчиком или удалено из-за нарушения правил.",
"live_stream": "Это прямая трансляция, которую нельзя загрузить во время активности.\nДождитесь окончания трансляции, затем попробуйте загрузить архивную версию.",
"playlist_error": "Не удается получить доступ к этому плейлисту. Он может быть частным, удаленным или пустым.\nПроверьте, существует ли плейлист и доступен ли он публично.",
"network_error": "Ошибка сетевого подключения. Проверьте подключение к Интернету и повторите попытку.\nЕсли проблема сохраняется, видеосервер может быть временно недоступен.",
"invalid_url": "Недопустимый или неподдерживаемый URL. Проверьте ссылку и повторите попытку.\nУбедитесь, что вы используете действительный URL YouTube, Vimeo или другой поддерживаемой платформы.",
"premium_content": "Для этого контента требуется YouTube Premium или членство в канале.\nВам необходимо войти с учетной записью, имеющей доступ к этому контенту.",
"copyright_blocked": "Это видео заблокировано из-за претензий по авторским правам.\nВладелец контента ограничил доступ к этому видео.",
"extraction_failed": "Не удалось извлечь информацию о видео. Это может быть временной проблемой.\nПовторите попытку через несколько минут или проверьте правильность ссылки на видео.",
"generic_error": "Не удалось извлечь информацию о видео. Проверьте вашу ссылку.\nТехнические детали: {error}"
},
"history": {
"title": "История Загрузок",
"clear_all": "Очистить Всё",
"clear_confirm_title": "Очистить Историю?",
"clear_confirm_message": "Вы уверены? Это нельзя отменить.",
"no_history": "История пока пуста",
"no_history_description": "Загрузки появятся здесь",
"loading": "Загрузка истории...",
"search_placeholder": "Поиск...",
"open_location": "Открыть Папку",
"redownload": "Загрузить Снова",
"remove": "Удалить",
"file_not_found": "Файл не найден",
"file_not_found_message": "Файл был перемещен или удален:\n{path}",
"downloaded_on": "Загружено: {date}",
"file_size": "Размер: {size}",
"audio_download": "Аудио",
"video_download": "Видео",
"remove_confirm_title": "Удалить?",
"remove_confirm_message": "Удалить из истории?\n\n{title}",
"item_removed": "Удалено",
"history_cleared": "История очищена",
"entries_count": "{count} загрузок",
"one_entry": "1 загрузка",
"redownload_confirm_title": "Загрузить Снова?",
"redownload_confirm_message": "Загрузить снова?\n\n{title}",
"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",
"current_version": "Текущая версия:",
"latest_version": "Последняя версия:",
"status_up_to_date": "✓ Обновлено",
"status_update_available": "⚠ Доступно обновление",
"status_not_installed": "✗ Не установлено",
"status_idle": "Нажмите 'Проверить версию' для начала",
"status_checking": "🔄 Проверка версии...",
"check_updates": "Проверить версию",
"check_failed": "Не удалось проверить версию. Проверьте подключение к Интернету.",
"description": "Проверьте версию FFmpeg и сравните ее с последней доступной версией.",
"guide_info": " Для установки или обновления FFmpeg, <a href='https://github.com/oop7/ffmpeg-install-guide'>нажмите здесь, чтобы просмотреть наше подробное руководство по установке</a>."
},
"deno": {
"setup_required": "Требуется настройка Deno",
"setup_description": "YTSage требует Deno для запуска определенных функций.<br><br>Deno не найден в локальном каталоге приложения. YTSage необходимо настроить Deno для вашей системы {os_name}.<br><br>Нажмите 'Настроить Deno', чтобы загрузить и установить автоматически.",
"setup_button": "Настроить Deno",
"downloading": "Загрузка Deno...",
"extracting": "Извлечение Deno...",
"verifying": "Проверка Deno...",
"success": "Deno успешно установлен!",
"download_failed": "Ошибка загрузки",
"download_error": "Не удалось загрузить Deno: {error}",
"verification_failed": "Проверка SHA256 не удалась. Загруженный файл может быть поврежден или изменен.",
"setup_failed": "Не удалось настроить Deno. Некоторые функции могут работать неправильно.",
"setup_error": "Ошибка настройки"
},
"deno_updater": {
"title": "Проверка и Обновление Версии Deno",
"description": "Проверьте вашу версию Deno и обновите до последней версии.",
"current_version": "Текущая Версия:",
"latest_version": "Последняя Версия:",
"status_idle": "Нажмите 'Проверить Обновления', чтобы начать",
"status_checking": "🔄 Проверка обновлений...",
"status_up_to_date": "✓ Обновлено",
"status_update_available": "⚠ Доступно обновление",
"status_not_installed": "✗ Не установлено",
"check_updates": "Проверить Обновления",
"update_now": "Обновить Deno",
"updating": "🔄 Обновление Deno...",
"update_success": "✅ Deno успешно обновлен!",
"update_failed": "❌ Ошибка обновления: {error}",
"check_failed": "Не удалось проверить обновления. Проверьте подключение к Интернету."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "Türkçe (Turkish)",
"select_language": "Dil Seç:",
"current_language": "Mevcut dil: {language}",
"restart_required": "Dil değişikliği uygulamayı yeniden başlattıktan sonra geçerli olacaktır.",
"english": "İngilizce",
"spanish": "İspanyolca",
"portuguese": "Portekizce",
"russian": "Rusça",
"chinese": "Çince",
"german": "Almanca",
"french": "Fransızca",
"hindi": "Hintçe",
"indonesian": "Endonezce",
"turkish": "Türkçe",
"polish": "Lehçe",
"italian": "İtalyanca",
"arabic": "Arapça",
"japanese": "Japonca",
"help_text": "Arayüz için tercih ettiğiniz dili seçin.",
"restart_notice": "Dil değişikliği uygulamayı yeniden başlattıktan sonra geçerli olacaktır."
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Hazır"
},
"formats": {
"show_formats": "Formatları göster:",
"video_format": "Video formatı:",
"audio_format": "Ses formatı:",
"no_formats": "Kullanılabilir format yok",
"loading": "Formatlar yükleniyor...",
"select": "Seç",
"quality": "Kalite",
"extension": "Uzantı",
"resolution": "Çözünürlük",
"file_size": "Dosya boyutu",
"codec": "Codec",
"audio": "Ses",
"fps": "FPS",
"hdr": "HDR",
"will_merge_audio": "Ses birleştirilecek",
"has_audio": "✓ Ses var",
"audio_only": "Sadece ses",
"best_4k": "En iyi (4K)",
"best_2k": "En iyi (2K)",
"high_1080p": "Yüksek (1080p)",
"high_720p": "Yüksek (720p)",
"medium_480p": "Orta (480p)",
"low_quality": "Düşük kalite",
"best_audio": "En iyi ses",
"high_audio": "Yüksek ses",
"medium_audio": "Orta ses",
"low_audio": "Düşük ses",
"audio_only_resolution": "Sadece ses"
},
"buttons": {
"reset": "Sıfırla",
"download": "İndir",
"pause": "Duraklat",
"resume": "Devam Et",
"cancel": "İptal",
"browse": "Gözat",
"clear": "Temizle",
"ok": "Tamam",
"apply": "Uygula",
"close": "Kapat",
"run_command": "Komutu çalıştır",
"custom_command_help": "Yardım",
"about": "Hakkında",
"analyze": "Analiz Et",
"paste_url": "URL Yapıştır",
"select_videos": "Video seç...",
"video": "Video",
"audio_only": "Sadece ses",
"custom_options": "Özel seçenekler",
"trim_video": "Videoyu kırp",
"download_settings": "İndirme ayarları",
"update": "yt-dlp'yi güncelle",
"select_defaults": "Varsayılanları seç",
"select_all": "Tümünü seç",
"deselect_all": "Tüm seçimi kaldır",
"open_folder": "Klasör konumunu aç",
"history": "Geçmiş",
"save_playlist": "Oynatma Listesini Farklı Kaydet"
},
"dialogs": {
"custom_options": "Özel seçenekler",
"settings": "Ayarlar",
"select_folder": "İndirme klasörünü seç",
"sponsorblock_categories": "SponsorBlock kategorileri",
"sponsorblock_description": "İndirme sırasında otomatik olarak kaldırılacak video bölümlerinin türlerini seçin.\nSponsorBlock bu bölümleri belirlemek için topluluk tarafından gönderilen verileri kullanır.",
"select_subtitles": "Altyazı seç",
"filter_languages_placeholder": "Dilleri filtrele (örn: tr, en)...",
"no_subtitles_available": "Altyazı mevcut değil",
"matching": "eşleşen",
"ytdlp_log_title": "yt-dlp Günlüğü",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "Çerezlerle giriş yap",
"custom_command": "Özel komut",
"proxy": "Proxy",
"language": "Dil",
"updater": "Güncelleyici"
},
"cookies": {
"help_text": "Kimlik doğrulama için çerezlerin nasıl sağlanacağını seçin.\nBu, özel videoları ve yüksek kaliteli ses dosyalarını indirmeyi sağlar.",
"cookie_source": "Çerez kaynağı",
"use_cookie_file": "Çerez dosyası kullan",
"extract_from_browser": "Tarayıcıdan çıkar",
"recommended": "Önerilen",
"cookie_file": "Çerez dosyası",
"cookie_file_placeholder": "cookies.txt dosya yolu...",
"browser_selection": "Tarayıcı seçimi",
"browser_help": "Çerezleri çıkaracak tarayıcıyı seçin:",
"browser_label": "Tarayıcı:",
"profile_label": "Profil:",
"profile_placeholder": "Varsayılan",
"browser_extract_message": "Tarayıcı çerezleri uygulandığında çıkarılacak",
"file_selected_message": "Çerez dosyası seçildi - Uygulamak için Tamam'a tıklayın",
"select_file_title": "Çerez dosyası seç",
"file_filter": "Çerez dosyaları (*.txt *.lwp)",
"file_selected_title": "Çerez Dosyası Uygulandı",
"file_applied_message": "Çerez dosyası uygulandı: {path}",
"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",
"active_browser": "✓ Etkin: Tarayıcı çerezleri ({browser})",
"active_file": "✓ Etkin: Çerez dosyası ({file})",
"none_active": "○ Etkin çerez yok",
"remember_settings": "Sonraki başlangıçta çerez ayarlarını hatırla"
},
"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.",
"input_label": "yt-dlp argümanları:",
"input_placeholder": "yt-dlp argümanlarını buraya girin...\n\nÖrnek: --extract-audio --audio-format mp3",
"output_label": "Komut çıktısı:",
"output_placeholder": "Komut çıktısı burada görünecek...",
"command_placeholder": "Özel yt-dlp komutunu girin...",
"command_help": "Mevcut yer tutucular:\n{url} - Video URL'si\n{output} - Çıktı klasörü\n\nÖrnek: --write-info-json --write-thumbnail",
"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ı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.",
"main_proxy": "Ana proxy",
"main_proxy_help": "Tüm indirmeler için birincil proxy sunucusu. HTTP/HTTPS ve SOCKS5 protokollerini destekler.",
"proxy_url_label": "Proxy URL:",
"proxy_url_placeholder": "http://proxy:port veya socks5://proxy:port",
"proxy_examples": "Örnekler:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Coğrafi atlama proxy'si",
"geo_proxy_help": "Coğrafi kısıtlamaları atlamak için ikincil proxy.",
"geo_proxy_url_label": "Coğrafi atlama proxy URL'si:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Ana proxy'yi temizle",
"clear_geo_proxy": "Coğrafi proxy'yi temizle",
"proxy_url": "Proxy URL",
"proxy_placeholder": "http://proxy:port veya socks5://proxy:port",
"geo_bypass": "Coğrafi atlama proxy'si (coğrafi kısıtlamalar için)",
"geo_bypass_placeholder": "http://proxy:port coğrafi engellemeyi atlamak için",
"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ı",
"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.",
"saved_main": "Saved main proxy: {proxy}",
"saved_geo": "Saved geo proxy: {proxy}"
},
"download": {
"preparing": "İndirme hazırlanıyor...",
"starting": "🚀 İndirme başlatılıyor...",
"fetching_info": "🔍 Video bilgileri alınıyor...",
"preparing_streams": "🎯 Video akışları hazırlanıyor...",
"downloading_audio": "⏬ Ses indiriliyor...",
"downloading_video": "⏬ Video indiriliyor...",
"downloading_subtitle": "⏬ Altyazı indiriliyor...",
"downloading": "⏬ İndiriliyor...",
"completed": "✅ İndirme tamamlandı!",
"video_completed": "✅ Video indirmesi tamamlandı!",
"audio_completed": "✅ Ses indirmesi tamamlandı!",
"subtitle_completed": "✅ Altyazı indirmesi tamamlandı!",
"completed_cleaning": "✅ İndirme tamamlandı! Temizleniyor...",
"cancelled": "İndirme iptal edildi",
"processing_playlist": "📋 Oynatma listesi verileri işleniyor...",
"paused": "İndirme duraklatıldı",
"resumed": "İndirme devam ettirildi",
"merging_formats": "✨ İşleme sonrası: Formatlar birleştiriliyor...",
"removing_sponsor_segments": "✨ İşleme sonrası: Sponsor segmentleri kaldırılıyor...",
"speed": "Hız",
"eta": "Kalan süre",
"please_enter_url": "Lütfen önce bir URL girin.",
"please_set_path": "Lütfen önce indirme yolunu ayarlayın.",
"please_enter_url_and_path": "Lütfen bir URL girin ve indirme yolunu ayarlayın.",
"please_select_format": "Lütfen önce bir format seçin.",
"downloading_fallback": "⚡ İndiriliyor..."
},
"update": {
"title": "yt-dlp'yi güncelle",
"checking": "Güncellemeler kontrol ediliyor...",
"update_available": "Güncelleme mevcut!\nMevcut sürüm: {current}\nEn son sürüm: {latest}",
"up_to_date": "yt-dlp güncel (Sürüm {version})",
"could_not_determine": "Sürümler belirlenemedi.",
"error_comparing": "Sürüm karşılaştırma hatası: {error}",
"update_available_failed": "Güncelleme mevcut! (Karşılaştırma başarısız)\nMevcut: {current}\nEn son: {latest}",
"updating": "Güncelleniyor...",
"initializing": "🚀 Güncelleme işlemi başlatılıyor...",
"checking_current": "🔍 Mevcut kurulum kontrol ediliyor...",
"found_at": "📍 yt-dlp bulundu: {path}",
"error_getting_path": "❌ yt-dlp yolu alma hatası: {error}",
"updating_binary": "📦 Uygulama yönetimli yt-dlp binary'si güncelleniyor...",
"update_failed": "❌ yt-dlp güncellemesi başarısız. Lütfen tekrar deneyin veya internet bağlantınızı kontrol edin.",
"binary_updated": "✅ Binary başarıyla güncellendi!",
"update_failed_stderr": "❌ yt-dlp güncellemesi başarısız: {error}",
"update_timeout": "❌ yt-dlp güncelleme zaman aşımı.",
"unexpected_error": "❌ Güncelleme sırasında beklenmeyen hata: {error}",
"already_up_to_date": "✅ yt-dlp zaten güncel!",
"update_success": "✅ yt-dlp başarıyla güncellendi!",
"already_latest": "yt-dlp güncel (sürüm {version})",
"network_error": "❌ Güncelleme sırasında ağ hatası: {error}",
"general_error": "❌ Güncelleme başarısız: {error}",
"update_in_progress_title": "Güncelleme Devam Ediyor",
"update_in_progress_message": "yt-dlp şu anda güncelleniyor. Lütfen bir dakika bekleyin."
},
"about": {
"title": "YTSage Hakkında",
"version": "Sürüm {version}",
"description": "Temiz PySide6 arayüzü ile modern YouTube indiricisi.",
"author": "Yapımcı: {author}",
"github": "GitHub: {repo}",
"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.",
"detected": "✓ Algılandı",
"missing": "✗ Eksik",
"not_available": "Mevcut değil"
},
"time_range": {
"title": "Videoyu kırp",
"time_range_group": "Zaman aralığı",
"start_time": "Başlama zamanı (SS:DD:SS)",
"end_time": "Bitiş zamanı (SS:DD:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "Başlama zamanı (SS:DD:SS)",
"end_time_placeholder": "Bitiş zamanı (SS:DD:SS)",
"force_keyframes": "Kesim noktalarında anahtar kareleri zorla",
"help_text": "Videoyu kırpmak için başlama ve bitiş zamanlarını ayarlayın.\nTam videoyu indirmek için boş bırakın.",
"invalid_format": "Geçersiz zaman formatı. SS:DD:SS formatını kullanın.",
"start_after_end": "Başlama zamanı bitiş zamanından sonra olamaz."
},
"settings": {
"title": "İndirme ayarları",
"download_path": "İndirme yolu",
"browse": "Gözat...",
"speed_limit": "Hız sınırı",
"speed_limit_placeholder": "Yok",
"generic_mode": "Genel mod",
"enable_generic_mode": "Genel modu etkinleştir (YouTube dışı siteleri destekle)",
"generic_mode_help": "Dailymotion, CBC Gem ve yt-dlp tarafından desteklenen diğer sitelerden indirmeye izin verir.",
"auto_update_ytdlp": "yt-dlp otomatik güncelleme",
"enable_auto_updates": "yt-dlp otomatik güncellemelerini etkinleştir",
"update_frequency": "Güncelleme sıklığı:",
"check_startup": "Her başlangıçta kontrol et (kontroller arası en az 1 saat)",
"check_daily": "Günlük kontrol et",
"check_weekly": "Haftalık kontrol et",
"check_updates_now": "yt-dlp'yi güncelle",
"update_check_title": "Güncelleme kontrolü",
"could_not_determine_version": "Mevcut yt-dlp sürümü belirlenemedi.",
"update_available_dialog": "Güncelleme mevcut!\n\nMevcut: {current}\nEn son: {latest}\n\nGüncellemeye devam etmek için Tamam'a tıklayın.",
"up_to_date_dialog": "yt-dlp güncel!\n\nMevcut sürüm: {version}",
"error_checking_updates": "Güncelleme kontrol hatası: {error}",
"settings_saved_title": "Ayarlar kaydedildi",
"settings_saved_message": "Otomatik güncelleme ayarları başarıyla kaydedildi!",
"error_title": "Hata",
"failed_save_settings": "Otomatik güncelleme ayarları kaydedilemedi.",
"error_saving_settings": "Otomatik güncelleme ayarları kaydetme hatası: {error}",
"ytdlp_channel": "yt-dlp Sürüm Kanalı",
"ytdlp_channel_stable": "Kararlı (Test edilmiş sürümler)",
"ytdlp_channel_nightly": "Nightly (Günlük güncellemeler, yt-dlp tarafından önerilir)",
"ytdlp_channel_description": "Kararlı ve nightly sürümler arasında seçim yapın. Nightly derlemeleri en son düzeltmeler ve özelliklerle günlük olarak güncellenir. İstediğiniz zaman kanal değiştirebilirsiniz.",
"ytdlp_switching_channel": "{channel} kanalına geçiliyor...",
"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_app_updates": "Başlangıçta YTSage güncellemelerini kontrol et",
"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.",
"current_status": "Mevcut durum",
"current_version_label": "Mevcut yt-dlp sürümü: Kontrol ediliyor...",
"last_check_label": "Son güncelleme kontrolü: Hiç",
"next_check_label": "Sonraki kontrol: Ayarlara göre",
"manual_check_button": "🔍 Şimdi güncellemeleri kontrol et",
"update_frequency_group": "Güncelleme sıklığı",
"save_settings": "Ayarları kaydet",
"settings_saved_successfully": "✅ Ayarlar başarıyla kaydedildi!",
"error_saving": "❌ Ayarları kaydetme hatası: {error}",
"output_format_settings": "Çıktı Formatı Ayarları",
"force_output_format": "Birleştirirken çıktı formatını zorla",
"preferred_format": "Tercih edilen format:",
"force_format_help": "Etkinleştirildiğinde, birleştirilen videolar tercih ettiğiniz formata dönüştürülür. yt-dlp'nin otomatik karar vermesini sağlamak için devre dışı bırakın.",
"format_mp4": "MP4 (En uyumlu)",
"format_webm": "WebM (Modern, açık)",
"format_mkv": "MKV (Özellik açısından zengin)",
"audio_format_settings": "Ses Formatı Ayarları",
"force_audio_format": "Yalnızca ses indirmeleri için ses formatını zorla",
"audio_normalization": "Ses Normalizasyonu (EBU R128)",
"audio_normalization_help": "Etkinleştirildiğinde ses parçaları normalleştirilecektir. Not: Bu, yeniden kodlamayı gerektirir, bu nedenle belirli bir ses formatı (MP3 veya M4A gibi) zorunlu kılınmalıdır.",
"preferred_audio_format": "Tercih edilen ses formatı:",
"force_audio_format_help": "Etkinleştirildiğinde, yalnızca ses indirmeleri tercih ettiğiniz formata dönüştürülecektir. Bu yalnızca ses formatları indirilirken geçerlidir.",
"audio_format_best": "En iyi (Dönüştürme yok)",
"audio_format_aac": "AAC (Düzenleme için iyi)",
"audio_format_mp3": "MP3 (Evrensel)",
"audio_format_flac": "FLAC (Kayıpsız)",
"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)",
"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.",
"tab_general": "Genel",
"tab_format": "Biçim",
"tab_file": "Dosya",
"concurrent_fragments": "Eşzamanlı Bağlantılar",
"concurrent_fragments_help": "İndirme başına bağlantı sayısı. Daha yüksek değerler sınırlamayı atlar ancak çok yüksek ayarlanırsa geçici engellemelere neden olabilir. Varsayılan: 1.",
"defaults_settings": "Varsayılan Seçim Ayarları",
"default_video_quality": "Varsayılan Video Çözünürlüğü (Yükseklik):",
"default_subtitle_language": "Varsayılan Altyazı Dili/Dilleri:",
"defaults_help": "Tercih ettiğiniz video yüksekliğini ve altyazı dillerini (virgülle ayırarak) ayarlayın. Varsa otomatik olarak seçileceklerdir."
},
"main_ui": {
"url_placeholder": "YouTube video veya oynatma listesi URL'sini girin",
"url_placeholder_generic": "Desteklenen herhangi bir siteden video veya oynatma listesi URL'si girin",
"merge_subtitles": "Altyazıları birleştir",
"save_thumbnail": "Küçük resmi kaydet",
"save_description": "Açıklamayı kaydet",
"embed_chapters": "Bölümleri göm",
"subtitles_selected": "{count} seçildi",
"all_selected": "Tümü seçildi",
"select_videos_all": "Video seç... (Tümü seçildi)",
"please_enter_url": "Lütfen önce bir URL girin",
"error_title": "Hata",
"cookie_file_selected_title": "Çerez dosyası seçildi",
"cookie_file_selected_message": "Çerez dosyası seçildi: {path}",
"browser_cookies_selected_title": "Tarayıcı çerezleri seçildi",
"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": "İ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_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",
"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",
"sponsor_desc": "Ücretli reklamlar, ücretli öneriler ve doğrudan reklamlar",
"selfpromo": "Ücretsiz/Kendi tanıtımı",
"selfpromo_desc": "Yaratıcının kendi içeriği için ücretsiz tanıtım",
"interaction": "Etkileşim hatırlatıcısı",
"interaction_desc": "İzleyicilerden beğeni, abone olma veya sosyal medya takibi istenir",
"intro": "Giriş",
"intro_desc": "Atlanabilir video girişi",
"outro": "Son/Son kartları",
"outro_desc": "Jenerik veya video bittiğinde",
"preview": "Önizleme/Özet",
"preview_desc": "Önceki videoların kısa özeti veya gelecek içeriğin önizlemesi",
"music_offtopic": "Müzik dışı bölüm",
"music_offtopic_desc": "Sadece müzik videoları için. Müzik dışı bölümleri işaretler",
"filler": "Dolgu teğet",
"filler_desc": "Sadece dolgu veya mizah için eklenen teğet sahneler"
},
"video_info": {
"channel": "Kanal",
"views": "👁 Görüntülenme",
"likes": "👍 Beğeni",
"upload_date": "📅 Yüklenme tarihi",
"duration": "⏱ Süre",
"unknown_channel": "Bilinmeyen kanal",
"unknown_date": "Bilinmeyen tarih",
"unknown_title": "Bilinmeyen başlık"
},
"command": {
"running": "Çalışıyor...",
"run_command": "Komutu çalıştır"
},
"selection": {
"none_selected": "0 seçildi",
"one_selected": "1 kategori seçildi",
"count_selected": "{count} seçildi"
},
"status": {
"ready": "Hazır",
"file_exists": "⚠️ Dosya zaten mevcut",
"video_file_exists": "⚠️ Video dosyası zaten mevcut",
"audio_file_exists": "⚠️ Ses dosyası zaten mevcut",
"subtitle_file_exists": "⚠️ Altyazı dosyası zaten mevcut",
"cancelling": "İndirme iptal ediliyor...",
"thumbnail_saved": "✅ Küçük resim kaydedildi: {filename}",
"thumbnail_error": "❌ Küçük resim hatası: {error}",
"thumbnail_no_image": "Kaydedilecek küçük resim yok"
},
"errors": {
"playlist_no_videos": "Hata: Oynatma listesi geçerli video içermiyor.",
"playlist_no_url": "Hata: Oynatma listesinin ilk videosu için URL alınamadı.",
"ytdlp_not_found": "Hata: yt-dlp çalıştırılabilir dosyası bulunamadı. Lütfen önce yt-dlp'yi yükleyin.",
"ytdlp_not_found_path": "Hata: yt-dlp çalıştırılabilir dosyası bulunamadı. Bu, hatalı kurulum veya PATH sorunu nedeniyle olabilir.",
"no_data_returned": "Hata: yt-dlp'den veri döndürülmedi",
"no_format_info": "Hata: Kullanılabilir format bilgisi yok.",
"analysis_timeout": "Hata: Analiz zaman aşımına uğradı. Lütfen tekrar deneyin.",
"invalid_speed_limit": "❌ Hata: Ayarlarda geçersiz hız sınırı değeri ayarlandı.",
"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}",
"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}",
"private_video": "Bu video gizli olabilir. Lütfen özel seçeneklerden çerezleri kullanın."
},
"update_dialog": {
"title": "Güncelleme Mevcut",
"new_version_available": "YTSage'in yeni bir sürümü mevcut!",
"current_version_label": "Mevcut sürüm:",
"latest_version_label": "En son sürüm:",
"changelog": "Değişiklik günlüğü",
"download_update": "Güncellemeyi İndir",
"remind_later": "Daha Sonra Hatırlat"
},
"playlist": {
"unknown": "Bilinmeyen Oynatma Listesi",
"total_videos": "Toplam Video: {count}",
"display_format": "Oynatma Listesi: {title} | {count} video",
"select_videos_title": "Oynatma Listesi Videolarını Seç",
"save_as": "Oynatma Listesini Farklı Kaydet",
"save_success_title": "Başarılı",
"saved_successfully": "Oynatma listesi başarıyla kaydedildi.",
"save_error_title": "Kaydetme Hatası",
"no_videos_to_save": "Hiçbir oynatma listesi girdisi toplanmadı!",
"save_error_msg": "Oynatma listesi kaydedilemedi."
},
"subtitle_selection": {
"count_selected": "{count} seçildi"
},
"file_exists_dialog": {
"title": "Dosya zaten mevcut",
"message": "Dosya zaten mevcut:\n{filename}",
"info": "Bu video zaten indirilmiş."
},
"auto_update": {
"last_check_never": "Son güncelleme kontrolü: Hiç",
"last_check": "Son güncelleme kontrolü: {time}",
"next_check_disabled": "Sonraki kontrol: Devre dışı",
"next_check_startup": "Sonraki kontrol: Başlangıçta",
"next_check_overdue": "Sonraki kontrol: Şimdi (gecikmiş)",
"next_check": "Sonraki kontrol: {time}",
"next_check_error": "Sonraki kontrol: Hesaplama hatası",
"checking": "🔄 Kontrol ediliyor...",
"check_now": "🔍 Şimdi güncellemeleri kontrol et",
"current_version": "Mevcut yt-dlp sürümü: {version}"
},
"url_validation": {
"empty_url": "URL boş olamaz",
"invalid_format": "Geçersiz URL formatı",
"invalid_scheme": "URL http:// veya https:// ile başlamalıdır",
"missing_domain": "Geçersiz URL: alan adı eksik",
"unsupported_platform": "YTSage yalnızca YouTube ve YouTube Music URL'lerini destekler.\n'{domain}' alan adı desteklenmiyor.",
"invalid_youtu_be": "Geçersiz youtu.be URL'si: video ID'si eksik"
},
"ytdlp_errors": {
"private_video": "Bu özel bir video. Çerezleri kullanarak hesabınıza giriş yaparak indirebilirsiniz.\nKimlik doğrulamak için 'Özel Seçenekler' → 'Çerezlerle Giriş' → 'Tarayıcıdan çerezleri çıkar' bölümüne gidin.",
"age_restricted": "Bu video yaş kısıtlamalı. Erişmek için giriş yapmanız gerekiyor.\nHesabınızla kimlik doğrulamak için 'Özel Seçenekler' → 'Çerezlerle Giriş' kullanın.",
"geo_blocked": "Bu video bölgenizde kullanılamıyor (coğrafi engel).\nBir VPN kullanmanız gerekebilir veya video ülkenizde kısıtlanmış olabilir.",
"video_unavailable": "Bu video kaldırıldı veya artık kullanılamıyor.\nVideo, yükleyici tarafından silinmiş veya politika ihlalleri nedeniyle kaldırılmış olabilir.",
"live_stream": "Bu, aktifken indirilemeyen canlı bir yayındır.\nYayın bitene kadar bekleyin, ardından arşivlenmiş sürümü indirmeyi deneyin.",
"playlist_error": "Bu oynatma listesine erişilemiyor. Özel, silinmiş veya boş olabilir.\nOynatma listesinin var olduğunu ve herkese açık olduğunu kontrol edin.",
"network_error": "Ağ bağlantı hatası. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin.\nSorun devam ederse, video sunucusu geçici olarak kullanılamıyor olabilir.",
"invalid_url": "Geçersiz veya desteklenmeyen URL. Lütfen bağlantıyı kontrol edin ve tekrar deneyin.\nGeçerli bir YouTube, Vimeo veya desteklenen başka bir platform URL'si kullandığınızdan emin olun.",
"premium_content": "Bu içerik YouTube Premium veya kanal üyeliği gerektirir.\nBu içeriğe erişimi olan bir hesapla giriş yapmanız gerekiyor.",
"copyright_blocked": "Bu video telif hakkı iddiaları nedeniyle engellenmiş.\nİçerik sahibi bu videoya erişimi kısıtlamış.",
"extraction_failed": "Video bilgileri çıkarılamadı. Bu geçici bir sorun olabilir.\nLütfen birkaç dakika içinde tekrar deneyin veya video bağlantısının doğru olup olmadığını kontrol edin.",
"generic_error": "Video bilgileri çıkarılamadı. Lütfen bağlantınızı kontrol edin.\nTeknik ayrıntılar: {error}"
},
"history": {
"title": "İndirme Geçmişi",
"clear_all": "Tümünü Temizle",
"clear_confirm_title": "Geçmişi Temizle?",
"clear_confirm_message": "Emin misiniz? Bu geri alınamaz.",
"no_history": "Henüz geçmiş yok",
"no_history_description": "İndirmeleriniz burada görünecek",
"search_placeholder": "Ara...",
"open_location": "Konumu Aç",
"redownload": "Tekrar İndir",
"remove": "Kaldır",
"file_not_found": "Dosya bulunamadı",
"file_not_found_message": "Dosya taşındı veya silindi:\n{path}",
"downloaded_on": "İndirildi: {date}",
"file_size": "Boyut: {size}",
"audio_download": "Ses",
"video_download": "Video",
"remove_confirm_title": "Kaldır?",
"remove_confirm_message": "Geçmişten kaldır?\n\n{title}",
"item_removed": "Kaldırıldı",
"history_cleared": "Geçmiş temizlendi",
"entries_count": "{count} indirme",
"one_entry": "1 indirme",
"redownload_confirm_title": "Tekrar İndir?",
"redownload_confirm_message": "Tekrar indir?\n\n{title}",
"redownload_started": "İndirme başladı",
"no_url_error": "Geçmiş kaydında URL bulunamadı",
"redownload_failed": "Yeniden indirme başlatılamadı: {error}",
"loading": "Loading history..."
},
"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ı.",
"already_installed": "FFmpeg zaten yüklü!",
"installation_complete": "Kurulum tamamlandı. Bu iletişim kutusunu kapatabilir ve YTSage'i kullanmaya devam edebilirsiniz.",
"installing": "FFmpeg kuruluyor... Lütfen bekleyin",
"install_success": "FFmpeg başarıyla kuruldu!",
"installation_complete_close": "Kurulum tamamlandı. Artık bu iletişim kutusunu kapatabilir ve YTSage'i kullanmaya devam edebilirsiniz.",
"try_manual": "Lütfen bunun yerine manuel kurulum kılavuzunu kullanmayı deneyin."
},
"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",
"current_version": "Mevcut Sürüm:",
"latest_version": "En Son Sürüm:",
"status_up_to_date": "✓ Güncel",
"status_update_available": "⚠ Güncelleme mevcut",
"status_not_installed": "✗ Yüklü değil",
"status_idle": "Başlamak için 'Sürümü Kontrol Et'e tıklayın",
"status_checking": "🔄 Sürüm kontrol ediliyor...",
"check_updates": "Sürümü Kontrol Et",
"check_failed": "Sürüm kontrolu başarısız oldu. İnternet bağlantınızı kontrol edin.",
"description": "FFmpeg sürümünüzü kontrol edin ve mevcut en son sürümle karşılaştırın.",
"guide_info": " FFmpeg'i yüklemek veya güncellemek için, <a href='https://github.com/oop7/ffmpeg-install-guide'>kapsamlı kurulum kılavuzumuzu görüntülemek için buraya tıklayın</a>."
},
"deno": {
"setup_required": "Deno Kurulumu Gerekli",
"setup_description": "YTSage, belirli özellikleri çalıştırmak için Deno gerektirir.<br><br>Deno, uygulamadaki yerel dizinde bulunamadı. YTSage'in {os_name} sisteminiz için Deno kurması gerekiyor.<br><br>Otomatik olarak indirmek ve yüklemek için 'Deno Kur'a tıklayın.",
"setup_button": "Deno Kur",
"downloading": "Deno indiriliyor...",
"extracting": "Deno çıkarılıyor...",
"verifying": "Deno doğrulanıyor...",
"success": "Deno başarıyla yüklendi!",
"download_failed": "İndirme Başarısız",
"download_error": "Deno indirilemedi: {error}",
"verification_failed": "SHA256 doğrulama başarısız oldu. İndirilen dosya bozuk veya kurcalanmış olabilir.",
"setup_failed": "Deno kurulamadı. Bazı özellikler düzgün çalışmayabilir.",
"setup_error": "Kurulum Hatası"
},
"deno_updater": {
"title": "Deno Sürüm Denetleyici ve Güncelleyici",
"description": "Deno sürümünüzü kontrol edin ve en son sürüme güncelleyin.",
"current_version": "Geçerli Sürüm:",
"latest_version": "En Son Sürüm:",
"status_idle": "Başlamak için 'Güncellemeleri Kontrol Et'e tıklayın",
"status_checking": "🔄 Güncellemeler kontrol ediliyor...",
"status_up_to_date": "✓ Güncel",
"status_update_available": "⚠ Güncelleme mevcut",
"status_not_installed": "✗ Yüklü değil",
"check_updates": "Güncellemeleri Kontrol Et",
"update_now": "Deno'yu Güncelle",
"updating": "🔄 Deno güncelleniyor...",
"update_success": "✅ Deno başarıyla güncellendi!",
"update_failed": "❌ Güncelleme başarısız: {error}",
"check_failed": "Güncellemeler kontrol edilemedi. Lütfen internet bağlantınızı kontrol edin."
}
}
+643
View File
@@ -0,0 +1,643 @@
{
"language": {
"display_name": "中文 (简体) (Chinese Simplified)",
"select_language": "选择语言:",
"current_language": "当前语言:{language}",
"restart_required": "语言更改将在重启应用程序后生效。",
"english": "英语",
"spanish": "西班牙语",
"portuguese": "葡萄牙语",
"russian": "俄语",
"chinese": "中文",
"german": "德语",
"french": "法语",
"hindi": "印地语",
"indonesian": "印尼语",
"turkish": "土耳其语",
"polish": "波兰语",
"italian": "意大利语",
"arabic": "阿拉伯语",
"japanese": "日语",
"help_text": "选择您喜欢的界面语言。",
"restart_notice": "语言更改将在重启应用程序后生效。"
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "准备就绪"
},
"formats": {
"show_formats": "显示格式:",
"video_format": "视频格式:",
"audio_format": "音频格式:",
"no_formats": "无可用格式",
"loading": "正在加载格式...",
"select": "选择",
"quality": "质量",
"extension": "扩展名",
"resolution": "分辨率",
"file_size": "文件大小",
"codec": "编解码器",
"audio": "音频",
"fps": "帧率",
"hdr": "HDR",
"will_merge_audio": "将合并音频",
"has_audio": "✓ 有音频",
"audio_only": "仅音频",
"best_4k": "最佳 (4K)",
"best_2k": "最佳 (2K)",
"high_1080p": "高清 (1080p)",
"high_720p": "高清 (720p)",
"medium_480p": "中等 (480p)",
"low_quality": "低质量",
"best_audio": "最佳音频",
"high_audio": "高质量音频",
"medium_audio": "中等音频",
"low_audio": "低质量音频",
"audio_only_resolution": "仅音频"
},
"buttons": {
"reset": "重置",
"download": "下载",
"pause": "暂停",
"resume": "恢复",
"cancel": "取消",
"browse": "浏览",
"clear": "清除",
"ok": "确定",
"apply": "应用",
"close": "关闭",
"run_command": "运行命令",
"custom_command_help": "帮助",
"about": "关于",
"analyze": "分析",
"paste_url": "粘贴网址",
"select_videos": "选择视频...",
"video": "视频",
"audio_only": "仅音频",
"custom_options": "自定义选项",
"trim_video": "裁剪视频",
"download_settings": "下载设置",
"update": "更新 yt-dlp",
"select_defaults": "选择默认值",
"select_all": "全选",
"deselect_all": "取消全选",
"open_folder": "打开文件夹位置",
"history": "历史",
"save_playlist": "保存播放列表为"
},
"dialogs": {
"custom_options": "自定义选项",
"settings": "设置",
"select_folder": "选择下载文件夹",
"sponsorblock_categories": "SponsorBlock 分类",
"sponsorblock_description": "选择在下载期间自动删除的视频片段类型。\nSponsorBlock 使用社区提交的数据来识别这些片段。",
"select_subtitles": "选择字幕",
"filter_languages_placeholder": "过滤语言(例如:en, zh...",
"no_subtitles_available": "无可用字幕",
"matching": "匹配",
"ytdlp_log_title": "yt-dlp 日志",
"filter_playlist_placeholder": "Filter videos..."
},
"tabs": {
"cookies": "使用 Cookie 登录",
"custom_command": "自定义命令",
"proxy": "代理",
"language": "语言",
"updater": "更新器"
},
"cookies": {
"help_text": "选择如何提供 cookie 来登录。\n这允许下载私人视频和高级质量音频。",
"cookie_source": "Cookie 来源",
"use_cookie_file": "使用 cookie 文件",
"extract_from_browser": "从浏览器提取",
"recommended": "推荐",
"cookie_file": "Cookie 文件",
"cookie_file_placeholder": "cookies.txt 文件路径...",
"browser_selection": "浏览器选择",
"browser_help": "选择要从中提取 cookie 的浏览器:",
"browser_label": "浏览器:",
"profile_label": "配置文件:",
"profile_placeholder": "默认",
"browser_extract_message": "应用时将提取浏览器 cookie",
"file_selected_message": "已选择 Cookie 文件 - 点击确定应用",
"select_file_title": "选择 Cookie 文件",
"file_filter": "Cookie 文件 (*.txt *.lwp)",
"file_selected_title": "已应用 Cookie 文件",
"file_applied_message": "已应用 Cookie 文件:{path}",
"browser_selected_title": "已应用浏览器 Cookie",
"browser_applied_message": "将从以下位置提取浏览器 cookie{browser}",
"cleared_title": "已清除 Cookie",
"cleared_message": "Cookie 设置已被清除",
"active_browser": "✓ Active: Browser cookies ({browser})",
"active_file": "✓ Active: Cookie file ({file})",
"none_active": "○ No cookies active",
"remember_settings": "在下次启动时记住 cookie 设置"
},
"custom_command": {
"help_text": "在下面输入您的自定义 yt-dlp 命令。当前网址将自动附加。<br><br>有关选项和使用示例的完整列表,<a href=\"{docs_url}\">点击此处查看官方 yt-dlp 文档</a>。<br><br>注意:下载路径和文件名模板将自动处理。",
"input_label": "yt-dlp 参数:",
"input_placeholder": "在此输入 yt-dlp 参数...\n\n例如:--extract-audio --audio-format mp3",
"output_label": "命令输出:",
"output_placeholder": "命令输出将显示在此处...",
"command_placeholder": "输入自定义 yt-dlp 命令...",
"command_help": "可用占位符:\n{url} - 视频网址\n{output} - 输出目录\n\n示例:--write-info-json --write-thumbnail",
"full_command": "🔧 完整命令:{command}",
"command_success": "✅ 自定义命令执行成功!",
"command_failed": "❌ 命令失败,退出代码 {code}",
"command_error": "❌ 执行自定义命令时出错:{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": "配置下载的代理设置。留空使用直接连接。",
"main_proxy": "主代理",
"main_proxy_help": "所有下载的主代理服务器。支持 HTTP/HTTPS 和 SOCKS5 协议。",
"proxy_url_label": "代理网址:",
"proxy_url_placeholder": "http://proxy:port 或 socks5://proxy:port",
"proxy_examples": "示例:\n• HTTPhttp://proxy.example.com:8080\n• SOCKS5socks5://proxy.example.com:1080",
"geo_proxy": "地理绕过代理",
"geo_proxy_help": "专门用于绕过地理限制的辅助代理。",
"geo_proxy_url_label": "地理绕过代理网址:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "清除主代理",
"clear_geo_proxy": "清除地理代理",
"proxy_url": "代理网址",
"proxy_placeholder": "http://proxy:port 或 socks5://proxy:port",
"geo_bypass": "地理绕过代理(用于地理限制)",
"geo_bypass_placeholder": "http://proxy:port 用于绕过地理封锁",
"invalid_main_url": "主代理网址格式无效",
"invalid_geo_url": "地理代理网址格式无效",
"main_configured": "主代理已配置",
"geo_configured": "地理代理已配置",
"set_title": "已设置代理",
"set_message": "主代理已设置并保存: {proxy}",
"geo_set_title": "已设置地理代理",
"geo_set_message": "地理验证代理已设置并保存: {proxy}",
"cleared_title": "代理设置已清除",
"cleared_message": "所有代理设置已清除并保存。",
"saved_main": "Saved main proxy: {proxy}",
"saved_geo": "Saved geo proxy: {proxy}"
},
"download": {
"preparing": "正在准备下载...",
"starting": "🚀 正在开始下载...",
"fetching_info": "🔍 正在获取视频信息...",
"preparing_streams": "🎯 正在准备视频流...",
"downloading_audio": "⏬ 正在下载音频...",
"downloading_video": "⏬ 正在下载视频...",
"downloading_subtitle": "⏬ 正在下载字幕...",
"downloading": "⏬ 正在下载...",
"completed": "✅ 下载完成!",
"video_completed": "✅ 视频下载完成!",
"audio_completed": "✅ 音频下载完成!",
"subtitle_completed": "✅ 字幕下载完成!",
"completed_cleaning": "✅ 下载完成!正在清理...",
"cancelled": "下载已取消",
"processing_playlist": "📋 正在处理播放列表数据...",
"paused": "下载已暂停",
"resumed": "下载已恢复",
"merging_formats": "✨ 后处理:合并格式...",
"removing_sponsor_segments": "✨ 后处理:移除赞助片段...",
"speed": "速度",
"eta": "预计时间",
"please_enter_url": "请先输入网址。",
"please_set_path": "请先设置下载路径。",
"please_enter_url_and_path": "请输入网址并设置下载路径。",
"please_select_format": "请先选择格式。",
"downloading_fallback": "⚡ 正在下载..."
},
"update": {
"title": "更新 yt-dlp",
"checking": "正在检查更新...",
"update_available": "有可用更新!\n当前版本:{current}\n最新版本:{latest}",
"up_to_date": "yt-dlp 已是最新版本(版本 {version}",
"could_not_determine": "无法确定版本。",
"error_comparing": "比较版本时出错:{error}",
"update_available_failed": "有可用更新!(比较失败)\n当前:{current}\n最新:{latest}",
"updating": "正在更新...",
"initializing": "🚀 初始化更新过程...",
"checking_current": "🔍 检查当前安装...",
"found_at": "📍 在以下位置找到 yt-dlp{path}",
"error_getting_path": "❌ 获取 yt-dlp 路径时出错:{error}",
"updating_binary": "📦 正在更新应用程序管理的 yt-dlp 二进制文件...",
"update_failed": "❌ 更新 yt-dlp 失败。请重试或检查您的互联网连接。",
"binary_updated": "✅ 二进制文件更新成功!",
"update_failed_stderr": "❌ yt-dlp 更新失败:{error}",
"update_timeout": "❌ yt-dlp 更新超时。",
"unexpected_error": "❌ 更新期间出现意外错误:{error}",
"already_up_to_date": "✅ yt-dlp已是最新版本!",
"update_success": "✅ yt-dlp 已成功更新!",
"already_latest": "yt-dlp 是最新版本(版本 {version}",
"network_error": "❌ 更新期间网络错误:{error}",
"general_error": "❌ 更新失败:{error}",
"update_in_progress_title": "正在更新",
"update_in_progress_message": "yt-dlp 正在更新中。请稍候。"
},
"about": {
"title": "关于 YTSage",
"version": "版本 {version}",
"description": "具有简洁 PySide6 界面的现代 YouTube 下载器。",
"author": "作者:{author}",
"github": "GitHub{repo}",
"system_info": "系统信息",
"loading": "🔄 正在加载系统信息...",
"refresh": "🔄",
"open_logs": "📂 日志",
"logs_tooltip": "打开应用程序日志文件夹",
"refreshing": "🔄 正在刷新...",
"refresh_failed": "刷新失败",
"refresh_failed_message": "无法刷新版本信息。",
"detected": "✓ 已检测到",
"missing": "✗ 缺失",
"not_available": "不可用"
},
"time_range": {
"title": "裁剪视频",
"time_range_group": "时间范围",
"start_time": "开始时间 (HH:MM:SS)",
"end_time": "结束时间 (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"start_time_placeholder": "开始时间 (HH:MM:SS)",
"end_time_placeholder": "结束时间 (HH:MM:SS)",
"force_keyframes": "在切割点强制关键帧",
"help_text": "设置开始和结束时间来裁剪视频。\n留空下载完整视频。",
"invalid_format": "时间格式无效。请使用 HH:MM:SS 格式。",
"start_after_end": "开始时间不能晚于结束时间。"
},
"settings": {
"title": "下载设置",
"download_path": "下载路径",
"browse": "浏览...",
"speed_limit": "速度限制",
"speed_limit_placeholder": "无",
"generic_mode": "通用模式",
"enable_generic_mode": "启用通用模式(支持非 YouTube 网站)",
"generic_mode_help": "允许从 Dailymotion、CBC Gem 以及其他受 yt-dlp 支持的网站下载。",
"auto_update_ytdlp": "自动更新 yt-dlp",
"enable_auto_updates": "启用 yt-dlp 自动更新",
"update_frequency": "更新频率:",
"check_startup": "每次启动时检查(检查间隔最少 1 小时)",
"check_daily": "每日检查",
"check_weekly": "每周检查",
"check_updates_now": "更新 yt-dlp",
"update_check_title": "更新检查",
"could_not_determine_version": "无法确定当前 yt-dlp 版本。",
"update_available_dialog": "有可用更新!\\n\\n当前:{current}\\n最新:{latest}\\n\\n点击确定继续更新。",
"up_to_date_dialog": "yt-dlp 已是最新版本!\n\n当前版本:{version}",
"error_checking_updates": "检查更新时出错:{error}",
"settings_saved_title": "设置已保存",
"settings_saved_message": "自动更新设置已成功保存!",
"error_title": "错误",
"failed_save_settings": "保存自动更新设置失败。",
"error_saving_settings": "保存自动更新设置时出错:{error}",
"ytdlp_channel": "yt-dlp 发布渠道",
"ytdlp_channel_stable": "稳定版(经过测试的版本)",
"ytdlp_channel_nightly": "夜间版(每日更新,yt-dlp 推荐)",
"ytdlp_channel_description": "在稳定版和夜间版之间选择。夜间构建版每天更新,包含最新修复和功能。您可以随时切换渠道。",
"ytdlp_switching_channel": "正在切换到 {channel} 渠道...",
"ytdlp_channel_switched": "✅ 成功切换到 {channel} 渠道!",
"ytdlp_channel_switch_failed": "❌ 切换渠道失败:{error}",
"ytdlp_current_channel": "当前渠道:{channel}",
"app_updates_title": "YTSage 更新",
"check_app_updates": "启动时检查 YTSage 更新",
"check_beta_updates": "接收测试版更新",
"auto_update_title": "自动更新设置",
"auto_update_header": "🔄 自动更新设置",
"auto_update_description": "为 yt-dlp 配置自动更新,以确保您始终拥有最新功能和错误修复。",
"current_status": "当前状态",
"current_version_label": "当前 yt-dlp 版本:检查中...",
"last_check_label": "上次更新检查:从未",
"next_check_label": "下次检查:基于设置",
"manual_check_button": "🔍 立即检查更新",
"update_frequency_group": "更新频率",
"save_settings": "保存设置",
"settings_saved_successfully": "✅ 设置保存成功!",
"error_saving": "❌ 保存设置时出错:{error}",
"output_format_settings": "输出格式设置",
"force_output_format": "合并时强制输出格式",
"preferred_format": "首选格式:",
"force_format_help": "启用后,合并的视频将转换为您的首选格式。禁用以让 yt-dlp 自动决定。",
"format_mp4": "MP4(最兼容)",
"format_webm": "WebM(现代、开放)",
"format_mkv": "MKV(功能丰富)",
"audio_format_settings": "音频格式设置",
"force_audio_format": "强制仅音频下载的音频格式",
"audio_normalization": "音频标准化 (EBU R128)",
"audio_normalization_help": "启用后,音轨将被标准化。 注意:这需要重新编码,因此必须强制使用特定的音频格式(如 MP3 或 M4A)。",
"preferred_audio_format": "首选音频格式:",
"force_audio_format_help": "启用后,仅音频下载将转换为您的首选格式。这仅在下载音频格式时适用。",
"audio_format_best": "最佳(不转换)",
"audio_format_aac": "AAC(适合编辑)",
"audio_format_mp3": "MP3(通用)",
"audio_format_flac": "FLAC(无损)",
"audio_format_wav": "WAV(未压缩)",
"audio_format_opus": "Opus(高效)",
"audio_format_m4a": "M4AApple",
"audio_format_vorbis": "Vorbis(开放)",
"filename_format": "输出文件名格式",
"filename_format_help": "可用变量: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s。支持标准的 yt-dlp 输出模板语法。",
"tab_general": "一般",
"tab_format": "格式",
"tab_file": "文件",
"concurrent_fragments": "并发连接数",
"concurrent_fragments_help": "每个下载的连接数。较高的值可以绕过限速,但如果设置得太高,可能会导致临时被封。默认值:1。",
"defaults_settings": "默认选择设置",
"default_video_quality": "默认视频分辨率 (高度):",
"default_subtitle_language": "默认字幕语言:",
"defaults_help": "设置您首选的视频高度和字幕语言(用逗号分隔)。如果可用,它们将被自动选择。"
},
"main_ui": {
"url_placeholder": "输入 YouTube 视频或播放列表网址",
"url_placeholder_generic": "输入任意受支持网站的视频或播放列表网址",
"merge_subtitles": "合并字幕",
"save_thumbnail": "保存缩略图",
"save_description": "保存描述",
"embed_chapters": "嵌入章节",
"subtitles_selected": "已选择 {count} 个",
"all_selected": "全部已选择",
"select_videos_all": "选择视频...(全部已选择)",
"please_enter_url": "请先输入网址",
"error_title": "错误",
"cookie_file_selected_title": "已选择 Cookie 文件",
"cookie_file_selected_message": "已选择 Cookie 文件:{path}",
"browser_cookies_selected_title": "已选择浏览器 Cookie",
"browser_cookies_selected_message": "将从以下浏览器提取 Cookie{browser}",
"error_no_format_info": "错误:无可用格式信息。",
"error_extract_info": "错误:无法提取基本视频信息。请检查您的链接。",
"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_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": "请先选择字幕",
"settings_tooltip": "当前路径: {path}\n速度限制: {speed_limit}",
"speed_limit_none": "无",
"open_folder_error": "无法打开文件夹: {error}",
"time_range_set": "已设置区间: {section}"
},
"sponsorblock": {
"sponsor": "赞助商",
"sponsor_desc": "付费推广、付费推荐和直接广告",
"selfpromo": "非付费/自我推广",
"selfpromo_desc": "创作者对自己内容的非付费推广",
"interaction": "互动提醒",
"interaction_desc": "要求观众点赞、订阅或关注社交媒体",
"intro": "开头",
"intro_desc": "可以跳过的视频开头",
"outro": "结尾/片尾卡片",
"outro_desc": "制作人员名单或视频结束时",
"preview": "预览/回顾",
"preview_desc": "对以前视频的快速回顾或即将到来内容的预览",
"music_offtopic": "非音乐部分",
"music_offtopic_desc": "仅适用于音乐视频。标记非音乐部分",
"filler": "填充切线",
"filler_desc": "仅为填充或幽默而添加的切线场景"
},
"video_info": {
"channel": "频道",
"views": "👁 观看次数",
"likes": "👍 点赞数",
"upload_date": "📅 上传日期",
"duration": "⏱ 时长",
"unknown_channel": "未知频道",
"unknown_date": "未知日期",
"unknown_title": "未知标题"
},
"command": {
"running": "正在运行...",
"run_command": "运行命令"
},
"selection": {
"none_selected": "未选择",
"one_selected": "已选择 1 个分类",
"count_selected": "已选择 {count} 个"
},
"status": {
"ready": "准备就绪",
"file_exists": "⚠️ 文件已存在",
"video_file_exists": "⚠️ 视频文件已存在",
"audio_file_exists": "⚠️ 音频文件已存在",
"subtitle_file_exists": "⚠️ 字幕文件已存在",
"cancelling": "正在取消下载...",
"thumbnail_saved": "✅ 缩略图已保存: {filename}",
"thumbnail_error": "❌ 缩略图错误: {error}",
"thumbnail_no_image": "没有可保存的缩略图"
},
"errors": {
"playlist_no_videos": "错误:播放列表不包含有效视频。",
"playlist_no_url": "错误:无法获取播放列表第一个视频的URL。",
"ytdlp_not_found": "错误:未找到yt-dlp可执行文件。请先安装yt-dlp。",
"ytdlp_not_found_path": "错误:未找到yt-dlp可执行文件。这可能是由于安装不当或PATH问题。",
"no_data_returned": "错误:yt-dlp未返回数据",
"no_format_info": "错误:无可用格式信息。",
"analysis_timeout": "错误:分析超时。请重试。",
"invalid_speed_limit": "❌ 错误:设置中设置了无效的速度限制值。",
"ytdlp_failed": "错误:yt-dlp失败:{error}",
"parse_failed": "错误:解析yt-dlp输出失败:{error}",
"analysis_failed": "错误:分析失败:{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}",
"private_video": "此视频可能是私享视频。请使用自定义选项中的 cookie。"
},
"update_dialog": {
"title": "有可用更新",
"new_version_available": "YTSage 有新版本可用!",
"current_version_label": "当前版本:",
"latest_version_label": "最新版本:",
"changelog": "更新日志",
"download_update": "下载更新",
"remind_later": "稍后提醒"
},
"playlist": {
"unknown": "未知播放列表",
"total_videos": "总视频数:{count}",
"display_format": "播放列表:{title} | {count}个视频",
"select_videos_title": "选择播放列表视频",
"save_as": "保存播放列表为",
"save_success_title": "成功",
"saved_successfully": "播放列表已成功保存。",
"save_error_title": "保存错误",
"no_videos_to_save": "未收集到播放列表条目!",
"save_error_msg": "保存播放列表失败。"
},
"subtitle_selection": {
"count_selected": "已选择{count}个"
},
"file_exists_dialog": {
"title": "文件已存在",
"message": "文件已存在:\n{filename}",
"info": "该视频已被下载。"
},
"auto_update": {
"last_check_never": "上次检查更新: 从未",
"last_check": "上次检查更新: {time}",
"next_check_disabled": "下次检查: 已禁用",
"next_check_startup": "下次检查: 启动时",
"next_check_overdue": "下次检查: 现在 (已逾期)",
"next_check": "下次检查: {time}",
"next_check_error": "下次检查: 计算错误",
"checking": "🔄 检查中...",
"check_now": "🔍 立即检查更新",
"current_version": "当前 yt-dlp 版本:{version}"
},
"url_validation": {
"empty_url": "URL不能为空",
"invalid_format": "无效的URL格式",
"invalid_scheme": "URL必须以http://或https://开头",
"missing_domain": "无效的URL:缺少域名",
"unsupported_platform": "YTSage仅支持YouTube和YouTube Music的URL。\n不支持域名'{domain}'。",
"invalid_youtu_be": "无效的youtu.be URL:缺少视频ID"
},
"ytdlp_errors": {
"private_video": "这是一个私人视频。您可以通过使用Cookie登录您的账户来下载它。\n转到【自定义选项】→【使用Cookie登录】→【从浏览器提取Cookie】进行身份验证。",
"age_restricted": "此视频有年龄限制。您需要登录才能访问它。\n使用【自定义选项】→【使用Cookie登录】以使用您的账户进行身份验证。",
"geo_blocked": "此视频在您所在的地区不可用(地理封锁)。\n您可能需要使用VPN,或者该视频在您的国家受到限制。",
"video_unavailable": "此视频已被删除或不再可用。\n该视频可能已被上传者删除或因违反政策而被删除。",
"live_stream": "这是一个在活动时无法下载的直播。\n等待直播结束,然后尝试下载存档版本。",
"playlist_error": "无法访问此播放列表。它可能是私有的、已删除或为空。\n检查播放列表是否存在并且可以公开访问。",
"network_error": "网络连接错误。请检查您的互联网连接并重试。\n如果问题仍然存在,视频服务器可能暂时不可用。",
"invalid_url": "无效或不支持的URL。请检查链接并重试。\n确保您使用的是有效的YouTube、Vimeo或其他支持的平台URL。",
"premium_content": "此内容需要YouTube Premium或频道会员资格。\n您需要使用有权访问此内容的账户登录。",
"copyright_blocked": "由于版权声明,此视频已被封锁。\n内容所有者已限制对此视频的访问。",
"extraction_failed": "提取视频信息失败。这可能是一个临时问题。\n请在几分钟后重试,或检查视频链接是否正确。",
"generic_error": "无法提取视频信息。请检查您的链接。\n技术详情:{error}"
},
"history": {
"title": "下载历史",
"clear_all": "清除全部",
"clear_confirm_title": "清除历史?",
"clear_confirm_message": "确定吗?此操作无法撤消。",
"no_history": "暂无历史",
"no_history_description": "下载将显示在这里",
"search_placeholder": "搜索...",
"open_location": "打开位置",
"redownload": "重新下载",
"remove": "删除",
"file_not_found": "文件未找到",
"file_not_found_message": "文件已移动或删除:\n{path}",
"downloaded_on": "下载于:{date}",
"file_size": "大小:{size}",
"audio_download": "音频",
"video_download": "视频",
"remove_confirm_title": "删除?",
"remove_confirm_message": "从历史中删除?\n\n{title}",
"item_removed": "已删除",
"history_cleared": "历史已清除",
"entries_count": "{count} 个下载",
"one_entry": "1 个下载",
"redownload_confirm_title": "重新下载?",
"redownload_confirm_message": "重新下载?\n\n{title}",
"redownload_started": "已开始下载",
"no_url_error": "历史记录中未找到 URL",
"redownload_failed": "无法开始重新下载: {error}",
"loading": "Loading history..."
},
"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 可执行文件",
"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 版本检查器",
"current_version": "当前版本:",
"latest_version": "最新版本:",
"status_up_to_date": "✓ 已是最新",
"status_update_available": "⚠ 有可用更新",
"status_not_installed": "✗ 未安装",
"status_idle": "点击「检查版本」开始",
"status_checking": "🔄 正在检查版本...",
"check_updates": "检查版本",
"check_failed": "检查版本失败。请检查您的网络连接。",
"description": "检查您的 FFmpeg 版本并与最新可用版本进行比较。",
"guide_info": " 要安装或更新 FFmpeg<a href='https://github.com/oop7/ffmpeg-install-guide'>请点击此处查看我们的综合安装指南</a>。"
},
"deno": {
"setup_required": "需要设置 Deno",
"setup_description": "YTSage 需要 Deno 来运行某些功能。<br><br>在应用程序的本地目录中找不到 Deno。YTSage 需要为您的 {os_name} 系统设置 Deno。<br><br>点击“设置 Deno”以自动下载和安装。",
"setup_button": "设置 Deno",
"downloading": "正在下载 Deno...",
"extracting": "正在解压 Deno...",
"verifying": "正在验证 Deno...",
"success": "Deno 已成功安装!",
"download_failed": "下载失败",
"download_error": "下载 Deno 失败:{error}",
"verification_failed": "SHA256 验证失败。下载的文件可能已损坏或被篡改。",
"setup_failed": "设置 Deno 失败。某些功能可能无法正常工作。",
"setup_error": "设置错误"
},
"deno_updater": {
"title": "Deno 版本检查器与更新器",
"description": "检查您的 Deno 版本并更新到最新版本。",
"current_version": "当前版本:",
"latest_version": "最新版本:",
"status_idle": "点击“检查更新”开始",
"status_checking": "🔄 正在检查更新...",
"status_up_to_date": "✓ 已是最新",
"status_update_available": "⚠ 有可用更新",
"status_not_installed": "✗ 未安装",
"check_updates": "检查更新",
"update_now": "更新 Deno",
"updating": "🔄 正在更新 Deno...",
"update_success": "✅ Deno 已成功更新!",
"update_failed": "❌ 更新失败:{error}",
"check_failed": "检查更新失败。请检查您的网络连接。"
}
}
+34
View File
@@ -0,0 +1,34 @@
import sys
from PySide6.QtWidgets import QApplication, QMessageBox
from .utils.ytsage_logger import logger
from .gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main
def show_error_dialog(message):
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setText("Application Error")
error_dialog.setInformativeText(message)
error_dialog.setWindowTitle("Error")
error_dialog.exec()
def main():
try:
logger.info("Starting YTSage application")
app = QApplication(sys.argv)
window = YTSageApp() # Instantiate the main application class
window.show()
logger.info("Application window shown, entering main loop")
sys.exit(app.exec())
except Exception as e:
logger.critical(f"Critical application error: {e}", exc_info=True)
show_error_dialog(f"Critical error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
"""
Utility modules for YTSage.
This package contains shared utilities, constants, logging, and configuration management.
"""
+223
View File
@@ -0,0 +1,223 @@
"""
Config Manager Module
=====================
This module provides **thread-safe** centralized management for application
configuration in YTSage. It handles reading, writing, and managing settings
stored in a JSON file, with support for nested keys via dot notation.
Thread safety is ensured using a reentrant lock (`RLock`), so multiple threads
can safely access or modify settings concurrently.
Features
--------
- Thread-safe operations for getting, setting, and deleting configuration values.
- Loads settings from a JSON config file (`APP_CONFIG_FILE`).
- Creates the config file with default values if missing or corrupt.
- Retrieves, sets, and deletes settings using dot-separated keys.
- Provides safe error handling with logging instead of raising exceptions.
- Persists updates back to disk automatically.
Usage
-----
from .ytsage_config_manager import ConfigManager
# Load settings (auto-loads if not already loaded)
download_path = ConfigManager.get("download_path")
# Update a value
ConfigManager.set("download_path", "D:/Downloads")
# Retrieve nested value
last_check = ConfigManager.get("cached_versions.ytdlp.last_check")
# Delete a key
ConfigManager.delete("cached_versions.ffmpeg.path")
Design Notes
------------
- Settings are stored in `ConfigManager.settings` (a dict).
- Default values are defined in `ConfigManager.default_config`.
- 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 defaults
when possible.
"""
import json
import threading
from pathlib import Path
from typing import Any, Dict, Optional
from .ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR
from .ytsage_logger import logger
class ConfigManager:
"""
Thread-safe configuration manager for YTSage.
Provides methods to load, save, get, set, and delete settings stored in a JSON file.
Supports nested keys via dot notation and automatically persists changes.
"""
_lock: threading.RLock = threading.RLock()
_config_file: Path = APP_CONFIG_FILE
_settings: Dict[str, Any] = {}
_default_config: Dict[str, Any] = {
"download_path": str(USER_HOME_DIR / "Downloads"),
"generic_mode": True,
"speed_limit_value": None,
"speed_limit_unit_index": 0,
"cookie_source": "browser", # "browser" or "file"
"cookie_browser": "chrome",
"cookie_browser_profile": "",
"cookie_file_path": None,
"cookie_active": False, # True only if user explicitly applied cookies
"last_used_cookie_file": None,
"proxy_url": None,
"geo_proxy_url": None,
"auto_update_ytdlp": True,
"auto_update_frequency": "daily",
"check_app_updates": True,
"check_beta_updates": False,
"last_update_check": 0,
"concurrent_fragments": 1,
"language": "en",
"ytdlp_channel": "stable",
"force_output_format": False,
"preferred_output_format": "mp4",
"force_audio_format": False,
"preferred_audio_format": "best",
"audio_normalization": False,
"filename_format": "%(title)s_%(resolution)s_[%(id)s].%(ext)s",
"window_geometry": None,
"window_state": None,
"cached_versions": {
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
},
}
@classmethod
def _load(cls) -> None:
"""
Loads configuration settings from a JSON file if it exists and is valid.
If the file is missing or corrupt, loads default settings and creates or overwrites the config file as needed.
Logs actions and errors during the process.
"""
with cls._lock:
if cls._config_file.exists():
try:
with open(cls._config_file, "r", encoding="utf-8") as f:
cls._settings = json.load(f)
logger.info("Config loaded from file.")
except json.JSONDecodeError:
cls._settings = cls._default_config.copy()
logger.warning("Config file corrupt, loaded defaults.")
else:
cls._settings = cls._default_config.copy()
cls._save()
logger.info("Config file not found, created default config.")
@classmethod
def _save(cls) -> None:
"""
Save current settings 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:
with open(cls._config_file, "w", encoding="utf-8") as f:
json.dump(cls._settings, f, indent=4)
logger.debug("Config saved to file.")
except (OSError, PermissionError) as e:
logger.exception(f"Failed to save config: {e}")
except Exception as e:
logger.exception(f"Unexpected error while saving config: {e}")
@classmethod
def get(cls, key: str) -> Optional[Any]:
"""
Retrieve a configuration value using a dotted key notation.
Args:
key (str): The dotted key string representing the path to the desired setting (e.g., "database.host").
Optional[Any]: The value associated with the given key, or None if the key does not exist.
Notes:
- If the configuration settings are not loaded, this method will load them before attempting retrieval.
- If any part of the dotted key path is missing, None is returned and a debug message is logged.
"""
with cls._lock:
if not cls._settings:
cls._load()
parts: list[str] = key.split(".")
value: Any = cls._settings
for part in parts:
if isinstance(value, dict) and part in value:
value = value[part]
else:
logger.debug(f"Config key '{key}' not found.")
return None
return value
@classmethod
def set(cls, key: str, value: Any) -> None:
"""
Sets a configuration value for a given key.
If the configuration settings are not loaded, loads them first.
Supports nested keys using dot notation (e.g., "database.host").
Updates the configuration dictionary with the provided value,
saves the updated settings, and logs the change.
Args:
key (str): The configuration key, possibly nested using dots.
value (Any): The value to set for the specified key.
Returns:
None
"""
with cls._lock:
if not cls._settings:
cls._load()
parts: list[str] = key.split(".")
d: Dict[str, Any] = cls._settings
for part in parts[:-1]:
d = d.setdefault(part, {})
d[parts[-1]] = value
cls._save()
logger.info(f"Config key '{key}' set to '{value}'.")
@classmethod
def delete(cls, key: str) -> None:
"""
Deletes a configuration key from the settings.
If the key is nested (dot-separated), traverses the settings dictionary accordingly.
If the key exists, removes it and saves the updated settings.
Logs the deletion or if the key was not found.
Args:
key (str): The dot-separated configuration key to delete.
Returns:
None
"""
with cls._lock:
if not cls._settings:
cls._load()
parts: list[str] = key.split(".")
d: Any = cls._settings
for part in parts[:-1]:
if part not in d:
logger.debug(f"Config key '{key}' not found for deletion.")
return
d = d[part]
if parts[-1] in d:
d.pop(parts[-1], None)
cls._save()
logger.info(f"Config key '{key}' deleted.")
else:
logger.debug(f"Config key '{key}' not found for deletion.")
+240
View File
@@ -0,0 +1,240 @@
"""
This module defines centralized constants used across the YTSage application.
By storing shared values in one place, it improves consistency, readability,
and maintainability of the codebase.
Constants include:
- Asset paths for icons and notification sounds.
- OS detection and platform-specific directory paths for application data, binaries, logs, and configuration.
- Download URLs for yt-dlp and ffmpeg binaries.
- SUBPROCESS_CREATIONFLAGS: Used to specify subprocess creation flags (e.g., subprocess.CREATE_NO_WINDOW on Windows to hide the console window).
Directories are automatically created when the module is imported, ensuring the required structure exists for the application.
YTSage application constants.
"""
import os
import platform
import subprocess
import sys
from pathlib import Path
# Handle resource paths for both development and installed package
def get_asset_path(asset_relative_path: str) -> Path:
"""
Get the absolute path to an asset file, works both in development and installed package.
Args:
asset_relative_path: Relative path to the asset (e.g., "assets/Icon/icon.png")
Returns:
Path: Absolute path to the asset file
"""
# Check if running as a frozen executable (PyInstaller, cx_Freeze, etc.)
if getattr(sys, "frozen", False):
# Running as a frozen executable
# Try sys._MEIPASS first (PyInstaller)
if hasattr(sys, "_MEIPASS"):
asset_path = Path(sys._MEIPASS) / asset_relative_path
if asset_path.exists():
return asset_path
# For cx_Freeze, assets are typically in lib/ directory next to the executable
executable_dir = Path(sys.executable).parent
# Try with lib/ prefix (cx_Freeze standard structure)
asset_path = executable_dir / "lib" / asset_relative_path
if asset_path.exists():
return asset_path
# Try directly in executable directory
asset_path = executable_dir / asset_relative_path
if asset_path.exists():
return asset_path
# Not frozen - try importlib.resources for installed packages
try:
# Use importlib.resources (standard in Python 3.9+)
import importlib.resources as resources
try:
# Navigate to the package root and then to the asset
package_path = resources.files('ytsage')
asset_path = package_path / asset_relative_path
if asset_path.is_file():
return Path(str(asset_path))
except (ImportError, AttributeError, FileNotFoundError):
pass
except Exception:
pass
# Fallback to relative path (for development environment)
current_file = Path(__file__)
# Go up from utils to ytsage root, then to asset
ytsage_root = current_file.parent.parent.parent
asset_path = ytsage_root / asset_relative_path
return asset_path
# Assets Constants
ICON_PATH: Path = get_asset_path("assets/Icon/icon.png")
SOUND_PATH: Path = get_asset_path("assets/sound/notification.mp3")
OS_NAME: str = platform.system() # Windows ; Darwin ; Linux
IS_FROZEN = getattr(sys, "frozen", False)
USER_HOME_DIR: Path = Path.home()
# OS Specific Constants
if OS_NAME == "Windows":
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
# Always use user data directory for app data, logs, config, and binaries
# Even when frozen, we don't want to create these folders next to the executable
APP_DIR: Path = Path(os.environ.get("LOCALAPPDATA", USER_HOME_DIR / "AppData" / "Local")) / "YTSage"
APP_BIN_DIR: Path = APP_DIR / "bin"
APP_DATA_DIR: Path = APP_DIR / "data"
APP_LOG_DIR: Path = APP_DIR / "logs"
APP_CONFIG_FILE: Path = APP_DATA_DIR / "ytsage_config.json"
APP_HISTORY_FILE: Path = APP_DATA_DIR / "ytsage_history.json"
APP_THUMBNAILS_DIR: Path = APP_DATA_DIR / "thumbnails"
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp.exe"
SUBPROCESS_CREATIONFLAGS: int = subprocess.CREATE_NO_WINDOW
elif OS_NAME == "Darwin": # macOS
_mac_version = platform.mac_ver()[0]
OS_FULL_NAME: str = f"macOS {_mac_version}" if _mac_version else "macOS"
# Always use user data directory for app data, logs, config, and binaries
# Even when frozen, we don't want to create these folders next to the executable
APP_DIR: Path = USER_HOME_DIR / "Library" / "Application Support" / "YTSage"
APP_BIN_DIR: Path = APP_DIR / "bin"
APP_DATA_DIR: Path = APP_DIR / "data"
APP_LOG_DIR: Path = APP_DIR / "logs"
APP_CONFIG_FILE: Path = APP_DATA_DIR / "ytsage_config.json"
APP_HISTORY_FILE: Path = APP_DATA_DIR / "ytsage_history.json"
APP_THUMBNAILS_DIR: Path = APP_DATA_DIR / "thumbnails"
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos"
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp"
SUBPROCESS_CREATIONFLAGS: int = 0
else: # Linux and other UNIX-like
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
# Always use user data directory for app data, logs, config, and binaries
# Even when frozen, we don't want to create these folders next to the executable
APP_DIR: Path = USER_HOME_DIR / ".local" / "share" / "YTSage"
APP_BIN_DIR: Path = APP_DIR / "bin"
APP_DATA_DIR: Path = APP_DIR / "data"
APP_LOG_DIR: Path = APP_DIR / "logs"
APP_CONFIG_FILE: Path = APP_DATA_DIR / "ytsage_config.json"
APP_HISTORY_FILE: Path = APP_DATA_DIR / "ytsage_history.json"
APP_THUMBNAILS_DIR: Path = APP_DATA_DIR / "thumbnails"
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
# Check for environment variable override (critical for Flatpak support)
_ytdlp_env_path = os.environ.get("YTDLP_APP_BIN_PATH")
if _ytdlp_env_path:
YTDLP_APP_BIN_PATH: Path = Path(_ytdlp_env_path)
else:
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp"
SUBPROCESS_CREATIONFLAGS: int = 0
# Documentation URLs
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
# yt-dlp SHA256 checksums URL
YTDLP_SHA256_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS"
# Deno download URLs and paths
if OS_NAME == "Windows":
DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip"
DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip.sha256sum"
DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno.exe"
elif OS_NAME == "Darwin": # macOS
DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-apple-darwin.zip"
DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-apple-darwin.zip.sha256sum"
DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno"
else: # Linux
DENO_DOWNLOAD_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip"
DENO_SHA256_URL: str = "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip.sha256sum"
# Check for environment variable override (critical for Flatpak support)
_deno_env_path = os.environ.get("DENO_APP_BIN_PATH")
if _deno_env_path:
DENO_APP_BIN_PATH: Path = Path(_deno_env_path)
else:
DENO_APP_BIN_PATH: Path = APP_BIN_DIR / "deno"
# FFmpeg download links (Essentials build - always latest version)
FFMPEG_7Z_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z"
FFMPEG_ZIP_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
FFMPEG_7Z_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.sha256"
FFMPEG_ZIP_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.sha256"
FFMPEG_7Z_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.ver"
FFMPEG_ZIP_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.ver"
# =============================================================================
# File Extension Constants
# =============================================================================
# Centralized file extension definitions to avoid duplication across modules
# Use these constants for file type detection throughout the application
# Video file extensions (container formats that typically contain video)
VIDEO_EXTENSIONS: frozenset[str] = frozenset({
".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"
})
# Audio file extensions (audio-only formats)
AUDIO_EXTENSIONS: frozenset[str] = frozenset({
".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"
})
# Subtitle file extensions
SUBTITLE_EXTENSIONS: frozenset[str] = frozenset({
".vtt", ".srt", ".ass", ".ssa"
})
# Combined video and audio extensions (for file search operations)
MEDIA_EXTENSIONS: frozenset[str] = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS
if __name__ == "__main__":
# If this file is run directly, print directory information; if imported, create the necessary directories for the application.
# for debug, to check os specific variable which can be different based on os.
info = {
"OS_NAME": OS_NAME,
"OS_FULL_NAME": OS_FULL_NAME,
"USER_HOME_DIR": str(USER_HOME_DIR),
"APP_DIR": str(APP_DIR),
"APP_BIN_DIR": str(APP_BIN_DIR),
"APP_DATA_DIR": str(APP_DATA_DIR),
"APP_LOG_DIR": str(APP_LOG_DIR),
"APP_CONFIG_FILE": str(APP_CONFIG_FILE),
"YTDLP_DOWNLOAD_URL": YTDLP_DOWNLOAD_URL,
"YTDLP_APP_BIN_PATH": YTDLP_APP_BIN_PATH,
"SUBPROCESS_CREATIONFLAGS": SUBPROCESS_CREATIONFLAGS,
}
for key, value in info.items():
print(f"{key}: {value}")
else:
APP_DIR.mkdir(parents=True, exist_ok=True)
APP_BIN_DIR.mkdir(parents=True, exist_ok=True)
APP_DATA_DIR.mkdir(parents=True, exist_ok=True)
APP_LOG_DIR.mkdir(parents=True, exist_ok=True)
APP_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
# Ensure custom yt-dlp directory exists if set
if OS_NAME not in ["Windows", "Darwin"]:
# Ensure parent directories exist for custom paths
if "YTDLP_APP_BIN_PATH" in globals():
YTDLP_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True)
if "DENO_APP_BIN_PATH" in globals():
DENO_APP_BIN_PATH.parent.mkdir(parents=True, exist_ok=True)
+428
View File
@@ -0,0 +1,428 @@
"""
History Manager Module
======================
This module provides **thread-safe** centralized management for download
history in YTSage using SQLite for high performance and scalability.
Features
--------
- Scalable: Uses SQLite instead of parsing potentially large JSON files.
- Thread-safe: Handles database connections safely.
- Migration: Automatically migrates legacy JSON history to SQLite.
- CRUD: Create, Read, Delete, Clear operations for history entries.
Usage
-----
from .ytsage_history_manager import HistoryManager
# Add a download to history
HistoryManager.add_entry(...)
# Get all history entries
history = HistoryManager.get_all_entries()
# Get recent entries (limit + offset support planned)
# Remove an entry
HistoryManager.remove_entry(entry_id)
# Clear all history
HistoryManager.clear_history()
"""
import json
import sqlite3
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from .ytsage_constants import APP_HISTORY_FILE, APP_DATA_DIR
from .ytsage_logger import logger
class HistoryManager:
"""
Thread-safe history manager for YTSage using SQLite.
"""
_lock = threading.RLock()
# Define DB file next to the old JSON file
_db_file = APP_DATA_DIR / "ytsage_history.db"
_connection = None
_initialized = False
@classmethod
def _init_db(cls):
"""Initialize the database: create table and migrate if needed."""
if cls._initialized:
return
with cls._lock:
# Check if file exists to know if we need to migrate or just create schema
db_exists = cls._db_file.exists()
legacy_json_exists = APP_HISTORY_FILE.exists()
try:
# Ensure directory exists
cls._db_file.parent.mkdir(parents=True, exist_ok=True)
# We use a persistent connection to avoid churn
if cls._connection is None:
cls._connection = sqlite3.connect(cls._db_file, check_same_thread=False)
cls._connection.row_factory = sqlite3.Row
cursor = cls._connection.cursor()
# Create table
cursor.execute("""
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
title TEXT,
url TEXT,
channel TEXT,
file_path TEXT,
download_date TEXT,
file_size INTEGER,
thumbnail_url TEXT,
format_id TEXT,
resolution TEXT,
is_audio_only INTEGER,
duration TEXT,
options TEXT,
timestamp REAL
)
""")
# Index for faster sorting by date
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_timestamp
ON history (timestamp DESC)
""")
# Indexes for faster search (title, channel, url)
# This prevents full table scans during search
cursor.execute("CREATE INDEX IF NOT EXISTS idx_title ON history (title)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_channel ON history (channel)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_url ON history (url)")
cls._connection.commit()
# If we just created the DB and have a JSON file, migrate
if not db_exists and legacy_json_exists:
cls._migrate_legacy_json()
cls._initialized = True
except sqlite3.Error as e:
logger.error(f"Failed to initialize history database: {e}")
@classmethod
def _migrate_legacy_json(cls):
"""Migrate legacy JSON history to SQLite."""
logger.info("Migrating legacy history JSON to SQLite...")
try:
with open(APP_HISTORY_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
count = 0
# Use the persistent connection
conn = cls._get_connection()
try:
with conn: # Transaction
cursor = conn.cursor()
for entry in data:
try:
# Safely extract download_options logic if complex
options_json = json.dumps(entry.get("download_options", {}))
# Construct timestamp from isoformat if missing
ts = entry.get("timestamp")
if not ts and "download_date" in entry:
try:
dt = datetime.fromisoformat(entry["download_date"])
ts = dt.timestamp()
except Exception:
ts = time.time()
cursor.execute("""
INSERT OR IGNORE INTO history (
id, title, url, channel, file_path, download_date,
file_size, thumbnail_url, format_id, resolution,
is_audio_only, duration, options, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
entry.get("id", str(int(time.time()*1000))),
entry.get("title", ""),
entry.get("url", ""),
entry.get("channel", "Unknown"),
entry.get("file_path", ""),
entry.get("download_date", ""),
entry.get("file_size", 0),
entry.get("thumbnail_url", ""),
entry.get("format_id", ""),
entry.get("resolution", ""),
1 if entry.get("is_audio_only") else 0,
entry.get("duration", ""),
options_json,
ts or time.time()
))
count += 1
except Exception as e:
logger.error(f"Skipped invalid entry during migration: {e}")
logger.info(f"Successfully migrated {count} history entries.")
# Rename old JSON to .bak to avoid re-migration, or keep as backup
try:
APP_HISTORY_FILE.rename(APP_HISTORY_FILE.with_suffix(".json.bak"))
except Exception as e:
logger.warning(f"Could not rename legacy history file: {e}")
except Exception as e:
logger.error(f"Migration transaction failed: {e}")
except Exception as e:
logger.error(f"Migration failed: {e}")
@classmethod
def _get_connection(cls):
"""Get the persistent database connection."""
cls._init_db()
if cls._connection is None:
# Should be created in _init_db, but just in case
cls._connection = sqlite3.connect(cls._db_file, check_same_thread=False)
cls._connection.row_factory = sqlite3.Row
return cls._connection
@classmethod
def get_all_entries(cls, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Get all history entries, sorted by most recent first.
Args:
limit: Optional limit on number of entries to return (most recent first)
Returns:
List of dictionary entries.
"""
entries = []
try:
with cls._lock: # Lock for simple concurrency safety
# Use persistent connection
conn = cls._get_connection()
# conn.row_factory is already set in _init_db/_get_connection
cursor = conn.cursor()
query = "SELECT * FROM history ORDER BY timestamp DESC"
params = ()
if limit is not None:
query += " LIMIT ?"
params = (limit,)
cursor.execute(query, params)
rows = cursor.fetchall()
for row in rows:
entry = dict(row)
# Convert boolean back
entry["is_audio_only"] = bool(entry["is_audio_only"])
# Parse options JSON
try:
entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {}
except json.JSONDecodeError:
entry["download_options"] = {}
del entry["options"] # Remove internal column
entries.append(entry)
except Exception as e:
logger.error(f"Error fetching history: {e}")
return entries
@classmethod
def get_entry(cls, entry_id: str) -> Optional[Dict[str, Any]]:
"""
Get a specific history entry by ID.
Args:
entry_id: The unique entry ID
Returns:
Entry dictionary or None if not found
"""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM history WHERE id = ?", (entry_id,))
row = cursor.fetchone()
if row:
entry = dict(row)
entry["is_audio_only"] = bool(entry["is_audio_only"])
try:
entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {}
except json.JSONDecodeError:
entry["download_options"] = {}
del entry["options"]
return entry
return None
except Exception as e:
logger.error(f"Error fetching entry {entry_id}: {e}")
return None
@classmethod
def add_entry(
cls,
title: str,
url: str,
thumbnail_url: Optional[str],
file_path: str,
format_id: str,
is_audio_only: bool,
resolution: str,
file_size: Optional[int] = None,
channel: Optional[str] = None,
duration: Optional[str] = None,
download_options: Optional[Dict[str, Any]] = None,
) -> str:
"""
Add a new entry to the download history.
"""
if download_options is None:
download_options = {}
timestamp = time.time()
# Ensure unique ID
unique_id = f"{int(timestamp * 1000)}"
download_date = datetime.fromtimestamp(timestamp).isoformat()
# Determine file size if not provided
if file_size is None:
try:
p = Path(file_path)
if p.exists():
file_size = p.stat().st_size
except Exception:
file_size = 0
# Allow None for optional strings
channel = channel or "Unknown"
duration = duration or ""
thumbnail_url = thumbnail_url or ""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO history (
id, title, url, channel, file_path, download_date,
file_size, thumbnail_url, format_id, resolution,
is_audio_only, duration, options, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
unique_id,
title,
url,
channel,
str(file_path),
download_date,
file_size,
thumbnail_url,
format_id,
resolution,
1 if is_audio_only else 0,
duration,
json.dumps(download_options),
timestamp
))
conn.commit()
logger.info(f"Added history entry: {title}")
return unique_id
except Exception as e:
logger.error(f"Error adding history entry: {e}")
return ""
@classmethod
def remove_entry(cls, entry_id: str) -> bool:
"""Remove an entry from history by ID."""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM history WHERE id = ?", (entry_id,))
if cursor.rowcount > 0:
conn.commit()
logger.info(f"Removed history entry: {entry_id}")
return True
return False
except Exception as e:
logger.error(f"Error removing history entry: {e}")
return False
@classmethod
def clear_history(cls) -> int:
"""Clear all history entries."""
try:
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM history")
count = cursor.rowcount
conn.commit()
logger.info("History cleared")
return count
except Exception as e:
logger.error(f"Error clearing history: {e}")
return 0
@classmethod
def search_entries(cls, query: str) -> List[Dict[str, Any]]:
"""
Search history entries by title, channel, or URL.
Args:
query: Search query string
Returns:
List of matching history entries
"""
if not query:
return cls.get_all_entries()
entries = []
try:
search_pattern = f"%{query}%"
with cls._lock:
conn = cls._get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM history
WHERE title LIKE ? OR channel LIKE ? OR url LIKE ?
ORDER BY timestamp DESC
""", (search_pattern, search_pattern, search_pattern))
rows = cursor.fetchall()
for row in rows:
entry = dict(row)
entry["is_audio_only"] = bool(entry["is_audio_only"])
try:
entry["download_options"] = json.loads(entry["options"]) if entry["options"] else {}
except json.JSONDecodeError:
entry["download_options"] = {}
del entry["options"]
entries.append(entry)
except Exception as e:
logger.error(f"Error searching history: {e}")
return entries
+302
View File
@@ -0,0 +1,302 @@
"""
Localization Manager Module
==========================
This module provides centralized localization support for YTSage application.
It handles loading language files, switching languages, and retrieving localized strings.
Features
--------
- Thread-safe operations for getting localized text
- Fallback to English when translation is missing
- Support for multiple languages via JSON files
- Dynamic language switching without restart
- Nested key support with dot notation
Usage
-----
from .ytsage_localization import LocalizationManager
# Get localized text
text = LocalizationManager.get_text("download.ready")
button_text = LocalizationManager.get_text("buttons.download")
# Change language
LocalizationManager.set_language("es")
# Get available languages
languages = LocalizationManager.get_available_languages()
"""
import json
import threading
from pathlib import Path
from typing import Any, Dict
from .ytsage_logger import logger
class LocalizationManager:
"""
Thread-safe localization manager for YTSage.
Handles loading, caching, and retrieving localized strings from JSON language files.
"""
_lock = threading.RLock()
_current_language = "en"
_languages: Dict[str, Dict[str, Any]] = {}
_languages_dir = Path(__file__).parent.parent / "languages"
# Fallback English strings embedded in code
_fallback_strings = {
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Ready"
},
"buttons": {
"download": "Download",
"pause": "Pause",
"resume": "Resume",
"cancel": "Cancel",
"browse": "Browse",
"clear": "Clear",
"ok": "OK",
"apply": "Apply",
"close": "Close"
},
"dialogs": {
"custom_options": "Custom Options",
"settings": "Settings"
},
"settings": {
"generic_mode": "Generic Mode",
"enable_generic_mode": "Enable Generic Mode (support non-YouTube sites)",
"generic_mode_help": "Allows downloading from Dailymotion, CBC Gem, and other sites supported by yt-dlp.",
"app_updates_title": "YTSage Updates",
"check_app_updates": "Check for YTSage updates on startup",
"check_beta_updates": "Receive Beta Updates"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"main_ui": {
"url_placeholder": "Enter YouTube video or playlist URL",
"url_placeholder_generic": "Enter video or playlist URL from any supported site",
"settings_tooltip": "Current Path: {path}\nSpeed Limit: {speed_limit}",
"speed_limit_none": "None"
},
"language": {
"select_language": "Select Language:",
"current_language": "Current language: {language}",
"restart_required": "Language changes will take effect after restarting the application.",
"english": "English",
"spanish": "Español (Spanish)",
"portuguese": "Português (Portuguese)",
"russian": "Русский (Russian)",
"chinese": "中文 (简体) (Chinese Simplified)",
"german": "Deutsch (German)",
"french": "Français (French)",
"hindi": "हिन्दी (Hindi)",
"indonesian": "Bahasa Indonesia (Indonesian)",
"turkish": "Türkçe (Turkish)",
"polish": "Polski (Polish)",
"italian": "Italiano (Italian)",
"arabic": "العربية (Arabic)",
"japanese": "日本語 (Japanese)"
},
"download": {
"preparing": "🚀 Preparing your download...",
"completed": "✅ Download completed!",
"video_completed": "✅ Video download completed!",
"audio_completed": "✅ Audio download completed!",
"subtitle_completed": "✅ Subtitle download completed!",
"please_set_path": "Please set a download path using 'Change Path'",
"please_enter_url": "Please enter a URL",
"please_enter_url_and_path": "Please enter URL and set download path",
"please_select_format": "Please select a format"
},
"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": "🔄"
}
}
@classmethod
def _ensure_languages_dir(cls) -> None:
"""Ensure the languages directory exists."""
cls._languages_dir.mkdir(exist_ok=True)
@classmethod
def _load_language(cls, language_code: str) -> Dict[str, Any]:
"""
Load a language file from disk.
Args:
language_code: The language code (e.g., 'en', 'es')
Returns:
Dictionary containing the language strings, or empty dict if not found
"""
language_file = cls._languages_dir / f"{language_code}.json"
if not language_file.exists():
logger.warning(f"Language file not found: {language_file}")
return {}
try:
with open(language_file, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
logger.error(f"Failed to load language file {language_file}: {e}")
return {}
@classmethod
def _get_nested_value(cls, data: Dict[str, Any], key: str) -> Any:
"""
Get a nested value from dictionary using dot notation.
Args:
data: Dictionary to search in
key: Dot-separated key (e.g., "app.title")
Returns:
The value if found, None otherwise
"""
parts = key.split(".")
value = data
for part in parts:
if isinstance(value, dict) and part in value:
value = value[part]
else:
return None
return value
@classmethod
def get_text(cls, key: str, **kwargs) -> str:
"""
Get localized text for the given key.
Args:
key: Dot-separated key for the text (e.g., "app.title")
**kwargs: Format parameters for the text
Returns:
Localized text, with fallback to English if not found
"""
with cls._lock:
# Load current language if not cached
if cls._current_language not in cls._languages:
cls._languages[cls._current_language] = cls._load_language(cls._current_language)
# Try to get from current language
current_lang_data = cls._languages.get(cls._current_language, {})
text = cls._get_nested_value(current_lang_data, key)
# Fallback to embedded English strings
if text is None:
text = cls._get_nested_value(cls._fallback_strings, key)
# Final fallback to key itself
if text is None:
logger.warning(f"Localization key not found: {key}")
text = key
# Format the text with provided parameters
if kwargs and isinstance(text, str):
try:
text = text.format(**kwargs)
except (KeyError, ValueError) as e:
logger.warning(f"Failed to format localized text '{key}': {e}")
return str(text)
@classmethod
def set_language(cls, language_code: str) -> None:
"""
Set the current language.
Args:
language_code: The language code to set (e.g., 'en', 'es')
"""
with cls._lock:
if language_code != cls._current_language:
cls._current_language = language_code
# Clear cache to force reload
cls._languages.clear()
logger.info(f"Language set to: {language_code}")
@classmethod
def get_current_language(cls) -> str:
"""Get the current language code."""
return cls._current_language
@classmethod
def get_available_languages(cls) -> Dict[str, str]:
"""
Get available languages from the languages directory.
Returns:
Dictionary mapping language codes to display names
"""
cls._ensure_languages_dir()
available_languages = {"en": cls.get_text("language.english")}
# Scan for language files
for language_file in cls._languages_dir.glob("*.json"):
lang_code = language_file.stem
if lang_code != "en": # Skip English as it's already added
# Try to get language display name from the file
lang_data = cls._load_language(lang_code)
display_name = cls._get_nested_value(lang_data, "language.display_name")
if display_name:
available_languages[lang_code] = display_name
else:
# Fallback display name
available_languages[lang_code] = lang_code.upper()
return available_languages
@classmethod
def initialize(cls, language_code: str = "en") -> None:
"""
Initialize the localization system.
Args:
language_code: Initial language code to use
"""
with cls._lock:
cls._ensure_languages_dir()
cls.set_language(language_code)
logger.info(f"Localization system initialized with language: {language_code}")
# Convenience function for getting localized text
def _(key: str, **kwargs) -> str:
"""
Convenience function to get localized text.
Args:
key: Dot-separated key for the text
**kwargs: Format parameters
Returns:
Localized text
"""
return LocalizationManager.get_text(key, **kwargs)
+56
View File
@@ -0,0 +1,56 @@
"""
YTSage centralized logging with loguru.
- This module provides centralized logging configuration for the entire YTSage application.
- Two log files: ytsage.log (all logs) & ytsage_error.log (errors only).
"""
import sys
from loguru import logger
from .ytsage_constants import APP_LOG_DIR, IS_FROZEN
# Separate configs for each handler
CONSOLE_CONFIG = {
"sink": sys.stdout if sys.stdout else sys.stderr,
"level": "INFO",
"colorize": True,
"enqueue": True,
}
ALL_LOGS_CONFIG = {
"sink": APP_LOG_DIR / "ytsage.log",
"level": "DEBUG",
"rotation": "10 MB",
"retention": "14 days",
"compression": "zip",
"enqueue": True,
}
ERROR_LOGS_CONFIG = {
"sink": APP_LOG_DIR / "ytsage_error.log",
"level": "ERROR",
"rotation": "5 MB",
"retention": "30 days",
"compression": "zip",
"enqueue": True,
}
# Logger initialization
def init_logger() -> None:
"""Configure loguru logger using separate configs for each handler."""
logger.remove() # Remove default loguru handler
if not IS_FROZEN:
logger.add(**CONSOLE_CONFIG)
logger.add(**ALL_LOGS_CONFIG)
logger.add(**ERROR_LOGS_CONFIG)
logger.info("YTSage logger initialized")
init_logger()
__all__ = ["logger"]