Merge branch 'beta'

This commit is contained in:
oop7
2025-10-25 18:42:47 +03:00
44 changed files with 7925 additions and 1184 deletions
+4 -4
View File
@@ -5,7 +5,7 @@ This repository uses GitHub Actions to automatically build and release YTSage fo
## How It Works ## How It Works
### Trigger ### Trigger
The workflow is triggered when you push a git tag that starts with `v` (e.g., `v4.8.0`, `v4.9.1`). The workflow is triggered when you push a git tag that starts with `v` (e.g., `v4.9.0`, `v4.9.1`).
### Build Process ### Build Process
1. **Setup**: Uses Python 3.13.6 on all platforms 1. **Setup**: Uses Python 3.13.6 on all platforms
@@ -21,13 +21,13 @@ The workflow is triggered when you push a git tag that starts with `v` (e.g., `v
2. **Commit your changes**: 2. **Commit your changes**:
```bash ```bash
git add . git add .
git commit -m "Release v4.8.0" git commit -m "Release v4.9.0"
``` ```
3. **Create and push a tag**: 3. **Create and push a tag**:
```bash ```bash
git tag v4.8.0 git tag v4.9.0
git push origin v4.8.0 git push origin v4.9.0
``` ```
4. **Watch the action**: Go to Actions tab in GitHub to monitor progress 4. **Watch the action**: Go to Actions tab in GitHub to monitor progress
+8 -5
View File
@@ -9,7 +9,7 @@ permissions:
contents: write contents: write
env: env:
PYTHON_VERSION: '3.13.6' PYTHON_VERSION: '3.13'
jobs: jobs:
build-linux: build-linux:
@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -31,12 +31,12 @@ jobs:
echo "VERSION=$version" >> "$GITHUB_OUTPUT" echo "VERSION=$version" >> "$GITHUB_OUTPUT"
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Cache Python dependencies - name: Cache Python dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
venv venv
@@ -122,7 +122,10 @@ jobs:
], ],
include_files=[ include_files=[
("src", "src"), ("src", "src"),
("assets", "lib/assets"), ("assets/branding/icons", "lib/assets/branding/icons"),
("assets/Icon", "lib/assets/Icon"),
("assets/sound", "lib/assets/sound"),
("languages", "lib/languages"),
("ytsage.desktop", "share/applications/ytsage.desktop"), ("ytsage.desktop", "share/applications/ytsage.desktop"),
("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"), ("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"),
], ],
+8 -5
View File
@@ -9,7 +9,7 @@ permissions:
contents: write contents: write
env: env:
PYTHON_VERSION: '3.13.6' PYTHON_VERSION: '3.13'
jobs: jobs:
build-macos: build-macos:
@@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -34,12 +34,12 @@ jobs:
echo "VERSION=$version" >> "$GITHUB_OUTPUT" echo "VERSION=$version" >> "$GITHUB_OUTPUT"
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Cache Python dependencies - name: Cache Python dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
venv venv
@@ -123,7 +123,10 @@ jobs:
], ],
include_files=[ include_files=[
("src", "src"), ("src", "src"),
("assets", "lib/assets"), ("assets/branding/icons", "lib/assets/branding/icons"),
("assets/Icon", "lib/assets/Icon"),
("assets/sound", "lib/assets/sound"),
("languages", "lib/languages"),
], ],
) )
+8 -5
View File
@@ -9,7 +9,7 @@ permissions:
contents: write contents: write
env: env:
PYTHON_VERSION: '3.13.6' PYTHON_VERSION: '3.13'
jobs: jobs:
build-windows: build-windows:
@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -31,12 +31,12 @@ jobs:
echo "VERSION=$version" >> $env:GITHUB_OUTPUT echo "VERSION=$version" >> $env:GITHUB_OUTPUT
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Cache Python dependencies - name: Cache Python dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
venv venv
@@ -114,7 +114,10 @@ jobs:
Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " ],"
Add-Content -Path "setup_cxfreeze.py" -Value " include_files=[" Add-Content -Path "setup_cxfreeze.py" -Value " include_files=["
Add-Content -Path "setup_cxfreeze.py" -Value ' ("src", "src"),' Add-Content -Path "setup_cxfreeze.py" -Value ' ("src", "src"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets", "lib/assets"),' Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/branding/icons", "lib/assets/branding/icons"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/Icon", "lib/assets/Icon"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets/sound", "lib/assets/sound"),'
Add-Content -Path "setup_cxfreeze.py" -Value ' ("languages", "lib/languages"),'
Add-Content -Path "setup_cxfreeze.py" -Value " ]," Add-Content -Path "setup_cxfreeze.py" -Value " ],"
Add-Content -Path "setup_cxfreeze.py" -Value ")" Add-Content -Path "setup_cxfreeze.py" -Value ")"
Add-Content -Path "setup_cxfreeze.py" -Value "" Add-Content -Path "setup_cxfreeze.py" -Value ""
+42 -3
View File
@@ -47,7 +47,7 @@ YTSage is designed for users who want a **simple yet powerful YouTube downloader
| ✨ Simple UI | 💾 Save Description | 🛠️ FFmpeg/yt-dlp Detection | | ✨ Simple UI | 💾 Save Description | 🛠️ FFmpeg/yt-dlp Detection |
| 📋 Playlist Support | 🖼️ Save thumbnail | ⚙️ Custom Commands | | 📋 Playlist Support | 🖼️ Save thumbnail | ⚙️ Custom Commands |
| 🖼️ Playlist Selector | 🚀 Speed Limiter | 🍪 Login with Cookies | | 🖼️ Playlist Selector | 🚀 Speed Limiter | 🍪 Login with Cookies |
| 📑 Embed Chapters | ✂️ Trim Video Sections | | | 📑 Embed Chapters | ✂️ Trim Video Sections | 🌐 Proxy Support |
</div> </div>
@@ -214,6 +214,28 @@ python main.py
- **Update yt-dlp:** Update yt-dlp - **Update yt-dlp:** Update yt-dlp
- **FFmpeg/yt-dlp Detection:** Automatically detect FFmpeg/yt-dlp - **FFmpeg/yt-dlp Detection:** Automatically detect FFmpeg/yt-dlp
- **Trim Video:** Download only specific parts of a video by specifying time ranges (HH:MM:SS format) - **Trim Video:** Download only specific parts of a video by specifying time ranges (HH:MM:SS format)
- **Proxy Support:** Use a proxy server for downloads (e.g., `http://<proxy-server>:<port>`)
</details>
<details>
<summary>🌍 Localization</summary>
YTSage supports **14 languages** for worldwide accessibility. Select your preferred language from **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` |
> 💡 **Want to contribute a translation?** Check out the [Contributing](#contributing) section to help us add more languages!
</details> </details>
@@ -330,6 +352,21 @@ YTSage/
│ │ └── icon.png │ │ └── icon.png
│ └── 📁 sound/ # Audio files │ └── 📁 sound/ # Audio files
│ └── notification.mp3 │ └── notification.mp3
├── 📁 languages/ # Localization files
│ ├── 📄 ar.json # Arabic translation
│ ├── 📄 de.json # German translation
│ ├── 📄 en.json # English translation
│ ├── 📄 es.json # Spanish translation
│ ├── 📄 fr.json # French translation
│ ├── 📄 hi.json # Hindi translation
│ ├── 📄 id.json # Indonesian translation
│ ├── 📄 it.json # Italian translation
│ ├── 📄 ja.json # Japanese translation
│ ├── 📄 pl.json # Polish translation
│ ├── 📄 pt.json # Portuguese translation
│ ├── 📄 ru.json # Russian translation
│ ├── 📄 tr.json # Turkish translation
│ └── 📄 zh.json # Chinese translation
├── 📄 LICENSE # License file ├── 📄 LICENSE # License file
├── 📄 main.py # Application entry point ├── 📄 main.py # Application entry point
├── 📄 README.md # Project documentation ├── 📄 README.md # Project documentation
@@ -340,7 +377,6 @@ YTSage/
│ ├── 📄 __init__.py # Core package init │ ├── 📄 __init__.py # Core package init
│ ├── 📄 ytsage_downloader.py # Download functionality │ ├── 📄 ytsage_downloader.py # Download functionality
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg integration │ ├── 📄 ytsage_ffmpeg.py # FFmpeg integration
│ ├── 📄 ytsage_logging.py # Logging utilities
│ ├── 📄 ytsage_style.py # UI styling │ ├── 📄 ytsage_style.py # UI styling
│ ├── 📄 ytsage_utils.py # Utility functions │ ├── 📄 ytsage_utils.py # Utility functions
│ └── 📄 ytsage_yt_dlp.py # yt-dlp integration │ └── 📄 ytsage_yt_dlp.py # yt-dlp integration
@@ -359,7 +395,10 @@ YTSage/
│ └── 📄 ytsage_dialogs_update.py # Update dialogs │ └── 📄 ytsage_dialogs_update.py # Update dialogs
└── 📁 utils/ # Utility modules └── 📁 utils/ # Utility modules
├── 📄 __init__.py # Utils package init ├── 📄 __init__.py # Utils package init
── 📄 ytsage_constants.py # Application constants ── 📄 ytsage_config_manager.py # Configuration management
├── 📄 ytsage_constants.py # Application constants
├── 📄 ytsage_localization.py # Localization utilities
└── 📄 ytsage_logger.py # Logging utilities
``` ```
</details> </details>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 KiB

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

After

Width:  |  Height:  |  Size: 815 KiB

+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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": "فتح موقع المجلد"
},
"dialogs": {
"custom_options": "خيارات مخصصة",
"settings": "الإعدادات",
"select_folder": "اختر مجلد التنزيل",
"sponsorblock_categories": "فئات SponsorBlock",
"sponsorblock_description": "اختر أنواع مقاطع الفيديو المراد إزالتها تلقائياً أثناء التنزيل.\nيستخدم SponsorBlock بيانات مقدمة من المجتمع لتحديد هذه المقاطع.",
"select_subtitles": "اختر الترجمات",
"filter_languages_placeholder": "تصفية اللغات (مثال: ar، en)...",
"no_subtitles_available": "لا توجد ترجمات متاحة",
"matching": "مطابقة"
},
"tabs": {
"cookies": "تسجيل الدخول بالكوكيز",
"custom_command": "أمر مخصص",
"proxy": "بروكسي",
"language": "اللغة"
},
"cookies": {
"help_text": "اختر طريقة لتوفير ملفات تعريف الارتباط للمصادقة.\nيسمح هذا بتنزيل مقاطع الفيديو الخاصة وملفات الصوت عالية الجودة.",
"cookie_source": "مصدر الكوكيز",
"use_cookie_file": "استخدام ملف كوكيز",
"extract_from_browser": "استخراج من المتصفح",
"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)"
},
"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}"
},
"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": "تم تكوين بروكسي الموقع"
},
"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 الثنائي المدار بواسطة التطبيق...",
"updating_pip": "🐍 جاري تحديث yt-dlp النظام عبر pip...",
"update_failed": "❌ فشل تحديث yt-dlp. حاول مرة أخرى أو تحقق من اتصال الإنترنت.",
"binary_updated": "✅ تم تحديث الملف الثنائي بنجاح!",
"update_failed_stderr": "❌ فشل تحديث yt-dlp: {error}",
"update_timeout": "❌ انتهت مهلة تحديث yt-dlp.",
"unexpected_error": "❌ خطأ غير متوقع أثناء التحديث: {error}",
"checking_pip": "🔍 جاري التحقق من تثبيت pip الحالي...",
"current_version": "📋 الإصدار الحالي: {version}",
"not_found_pip": "⚠️ لم يتم العثور على yt-dlp عبر pip، جاري محاولة التثبيت...",
"checking_latest": "🌐 جاري التحقق من أحدث إصدار...",
"failed_check_updates": "❌ فشل التحقق من التحديثات",
"latest_version": "🆕 أحدث إصدار: {version}",
"updating_from_to": "⬆️ جاري التحديث من {current} إلى {latest}...",
"running_pip_install": "📦 جاري تشغيل pip install --upgrade...",
"pip_completed": "✅ اكتمل تحديث pip بنجاح!",
"pip_failed": "❌ فشل تحديث pip: {error}",
"already_up_to_date": "✅ yt-dlp محدث بالفعل!",
"pip_timeout": "❌ انتهت مهلة تحديث pip بعد 5 دقائق",
"update_success": "✅ تم تحديث yt-dlp بنجاح!",
"already_latest": "yt-dlp محدث (الإصدار {version})",
"network_error": "❌ خطأ في الشبكة أثناء التحديث: {error}",
"general_error": "❌ فشل التحديث: {error}",
"error_pip_update": "❌ خطأ أثناء تحديث pip: {error}",
"pip_update_failed": "❌ فشل تحديث pip: {error}"
},
"about": {
"title": "حول YTSage",
"version": "الإصدار {version}",
"description": "برنامج تنزيل يوتيوب حديث مع واجهة PySide6 نظيفة.",
"author": "المطور: {author}",
"github": "GitHub: {repo}",
"system_info": "معلومات النظام",
"loading": "🔄 جاري تحميل معلومات النظام...",
"refresh": "🔄",
"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": "بدون",
"auto_update_ytdlp": "التحديثات التلقائية لـ yt-dlp",
"enable_auto_updates": "تفعيل التحديثات التلقائية لـ yt-dlp",
"update_frequency": "تكرار التحديثات:",
"check_startup": "تحقق عند كل بداية تشغيل (ساعة واحدة على الأقل بين الفحوصات)",
"check_daily": "تحقق يومياً",
"check_weekly": "تحقق أسبوعياً",
"check_updates_now": "تحقق من التحديثات الآن",
"update_check_title": "التحقق من التحديثات",
"could_not_determine_version": "تعذر تحديد الإصدار الحالي لـ yt-dlp.",
"update_available_dialog": "تحديث متاح!\n\nالحالي: {current}\nالأحدث: {latest}\n\nاستخدم زر 'تحديث yt-dlp' في النافذة الرئيسية للتحديث.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "أدخل رابط فيديو يوتيوب أو قائمة تشغيل",
"merge_subtitles": "دمج الترجمات",
"save_thumbnail": "حفظ الصورة المصغرة",
"save_description": "حفظ الوصف",
"embed_chapters": "تضمين الفصول",
"subtitles_selected": "{count} محدد",
"all_selected": "تم تحديد الكل",
"select_videos_all": "اختر الفيديوهات... (تم تحديد الكل)",
"please_enter_url": "الرجاء إدخال رابط أولاً",
"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": "التحليل (0%)... جاري التحضير للطلب",
"analyzing_extracting_basic": "التحليل (15%)... جاري استخراج المعلومات الأساسية",
"analyzing_extracting_detailed": "التحليل (30%)... جاري استخراج المعلومات التفصيلية",
"analyzing_processing_video": "التحليل (45%)... جاري معالجة بيانات الفيديو",
"analyzing_processing_formats": "التحليل (60%)... جاري معالجة التنسيقات",
"analyzing_loading_thumbnail": "التحليل (75%)... جاري تحميل الصورة المصغرة",
"analyzing_processing_subtitles": "التحليل (85%)... جاري معالجة الترجمات",
"analyzing_updating_table": "التحليل (95%)... جاري تحديث جدول التنسيقات",
"analysis_complete": "اكتمل التحليل!",
"analyzing_extracting_ytdlp": "التحليل (30%)... جاري استخراج المعلومات باستخدام yt-dlp",
"analyzing_processing_data": "التحليل (60%)... جاري معالجة البيانات",
"analyzing_processing_formats_ytdlp": "التحليل (75%)... جاري معالجة التنسيقات",
"analyzing_loading_thumbnail_ytdlp": "التحليل (85%)... جاري تحميل الصورة المصغرة",
"analyzing_processing_subtitles_ytdlp": "التحليل (90%)... جاري معالجة الترجمات",
"select_subtitles": "اختر الترجمات...",
"sponsorblock_categories": "فئات SponsorBlock...",
"invalid_url_or_enter": "عنوان URL غير صالح أو الرجاء إدخال عنوان URL.",
"zero_selected": "تم اختيار 0"
},
"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": "جاري إلغاء التنزيل..."
},
"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}"
},
"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": "اختر مقاطع الفيديو من قائمة التشغيل"
},
"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": "🔍 التحقق من التحديثات الآن"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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"
},
"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"
},
"tabs": {
"cookies": "Mit Cookies anmelden",
"custom_command": "Benutzerdefinierter Befehl",
"proxy": "Proxy",
"language": "Sprache"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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...",
"updating_pip": "🐍 System-yt-dlp wird über pip 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}",
"checking_pip": "🔍 Aktuelle pip-Installation wird überprüft...",
"current_version": "📋 Aktuelle Version: {version}",
"not_found_pip": "⚠️ yt-dlp über pip nicht gefunden, Installation wird versucht...",
"checking_latest": "🌐 Neueste Version wird überprüft...",
"failed_check_updates": "❌ Überprüfung auf Updates fehlgeschlagen",
"latest_version": "🆕 Neueste Version: {version}",
"updating_from_to": "⬆️ Update von {current} auf {latest}...",
"running_pip_install": "📦 pip install --upgrade wird ausgeführt...",
"pip_completed": "✅ Pip-Update erfolgreich abgeschlossen!",
"pip_failed": "❌ Pip-Update fehlgeschlagen: {error}",
"already_up_to_date": "✅ yt-dlp ist bereits auf dem neuesten Stand!",
"pip_timeout": "❌ Pip-Update nach 5 Minuten abgelaufen",
"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}",
"error_pip_update": "❌ Fehler während des Pip-Updates: {error}",
"pip_update_failed": "❌ Pip-Update fehlgeschlagen: {error}"
},
"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": "🔄",
"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",
"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": "Jetzt nach Updates suchen",
"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\nVerwenden Sie die Schaltfläche 'yt-dlp aktualisieren' im Hauptfenster zum Aktualisieren.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "YouTube-Video- oder Playlist-URL 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",
"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": "Analysiere (0%)... Anfrage wird vorbereitet",
"analyzing_extracting_basic": "Analysiere (15%)... Grundlegende Informationen werden extrahiert",
"analyzing_extracting_detailed": "Analysiere (30%)... Detaillierte Informationen werden extrahiert",
"analyzing_processing_video": "Analysiere (45%)... Videodaten werden verarbeitet",
"analyzing_processing_formats": "Analysiere (60%)... Formate werden verarbeitet",
"analyzing_loading_thumbnail": "Analysiere (75%)... Thumbnail wird geladen",
"analyzing_processing_subtitles": "Analysiere (85%)... Untertitel werden verarbeitet",
"analyzing_updating_table": "Analysiere (95%)... Formattabelle wird aktualisiert",
"analysis_complete": "Analyse abgeschlossen!",
"analyzing_extracting_ytdlp": "Analysiere (30%)... Informationen mit yt-dlp-Ausführbarer Datei werden extrahiert",
"analyzing_processing_data": "Analysiere (60%)... Daten werden verarbeitet",
"analyzing_processing_formats_ytdlp": "Analysiere (75%)... Formate werden verarbeitet",
"analyzing_loading_thumbnail_ytdlp": "Analysiere (85%)... Thumbnail wird geladen",
"analyzing_processing_subtitles_ytdlp": "Analysiere (90%)... Untertitel werden verarbeitet",
"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"
},
"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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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",
"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"
},
"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)...",
"no_subtitles_available": "No subtitles available",
"matching": "matching"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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...",
"updating_pip": "🐍 Updating system yt-dlp via pip...",
"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}",
"checking_pip": "🔍 Checking current pip installation...",
"current_version": "📋 Current version: {version}",
"not_found_pip": "⚠️ yt-dlp not found via pip, attempting installation...",
"checking_latest": "🌐 Checking for latest version...",
"failed_check_updates": "❌ Failed to check for updates",
"latest_version": "🆕 Latest version: {version}",
"updating_from_to": "⬆️ Updating from {current} to {latest}...",
"running_pip_install": "📦 Running pip install --upgrade...",
"pip_completed": "✅ Pip update completed successfully!",
"pip_failed": "❌ Pip update failed: {error}",
"pip_timeout": "❌ Pip update timed out after 5 minutes",
"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}",
"error_pip_update": "❌ Error during pip update: {error}",
"pip_update_failed": "❌ Pip update failed: {error}"
},
"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": "🔄",
"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",
"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": "Check for Updates Now",
"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\nUse the 'Update yt-dlp' button in the main window to 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}",
"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}"
},
"main_ui": {
"url_placeholder": "Enter YouTube video or playlist URL",
"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",
"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": "Analyzing (0%)... Preparing request",
"analyzing_extracting_basic": "Analyzing (15%)... Extracting basic info",
"analyzing_extracting_detailed": "Analyzing (30%)... Extracting detailed info",
"analyzing_processing_video": "Analyzing (45%)... Processing video data",
"analyzing_processing_formats": "Analyzing (60%)... Processing formats",
"analyzing_loading_thumbnail": "Analyzing (75%)... Loading thumbnail",
"analyzing_processing_subtitles": "Analyzing (85%)... Processing subtitles",
"analyzing_updating_table": "Analyzing (95%)... Updating format table",
"analysis_complete": "Analysis complete!",
"analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info with yt-dlp executable",
"analyzing_processing_data": "Analyzing (60%)... Processing data",
"analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats",
"analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail",
"analyzing_processing_subtitles_ytdlp": "Analyzing (90%)... Processing subtitles",
"select_subtitles": "Select Subtitles...",
"sponsorblock_categories": "SponsorBlock Categories...",
"invalid_url_or_enter": "Invalid URL or please enter a URL.",
"zero_selected": "0 selected"
},
"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..."
},
"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}",
"parse_failed": "Error: Failed to parse yt-dlp output: {error}",
"analysis_failed": "Error: Analysis failed: {error}",
"generic_error": "Error: {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"
},
"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"
}
}
+399
View File
@@ -0,0 +1,399 @@
{
"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"
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Listo"
},
"buttons": {
"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"
},
"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"
},
"tabs": {
"cookies": "Iniciar sesión con Cookies",
"custom_command": "Comando Personalizado",
"proxy": "Proxy",
"language": "Idioma"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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"
},
"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": "🔄",
"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)"
},
"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...",
"updating_pip": "🐍 Actualizando yt-dlp del sistema vía pip...",
"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}",
"checking_pip": "🔍 Verificando instalación actual de pip...",
"current_version": "📋 Versión actual: {version}",
"not_found_pip": "⚠️ yt-dlp no encontrado vía pip, intentando instalación...",
"checking_latest": "🌐 Verificando última versión...",
"failed_check_updates": "❌ Falló la verificación de actualizaciones",
"latest_version": "🆕 Última versión: {version}",
"updating_from_to": "⬆️ Actualizando de {current} a {latest}...",
"running_pip_install": "📦 Ejecutando pip install --upgrade...",
"pip_completed": "✅ ¡Actualización de pip completada exitosamente!",
"pip_failed": "❌ Actualización de pip falló: {error}",
"pip_timeout": "❌ Tiempo de espera agotado para actualización de pip después de 5 minutos",
"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}",
"error_pip_update": "❌ Error durante la actualización de pip: {error}",
"pip_update_failed": "❌ Actualización de pip falló: {error}"
},
"settings": {
"title": "Configuración de Descarga",
"download_path": "Ruta de Descarga",
"browse": "Examinar...",
"speed_limit": "Límite de Velocidad",
"speed_limit_placeholder": "Ninguno",
"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": "Verificar Actualizaciones Ahora",
"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\nUsa el botón 'Actualizar yt-dlp' en la ventana principal para actualizar.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "Ingresa URL de video o lista de YouTube",
"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",
"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": "Analizando (0%)... Preparando solicitud",
"analyzing_extracting_basic": "Analizando (15%)... Extrayendo información básica",
"analyzing_extracting_detailed": "Analizando (30%)... Extrayendo información detallada",
"analyzing_processing_video": "Analizando (45%)... Procesando datos de video",
"analyzing_processing_formats": "Analizando (60%)... Procesando formatos",
"analyzing_loading_thumbnail": "Analizando (75%)... Cargando miniatura",
"analyzing_processing_subtitles": "Analizando (85%)... Procesando subtítulos",
"analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos",
"analysis_complete": "¡Análisis completo!",
"analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo información con ejecutable yt-dlp",
"analyzing_processing_data": "Analizando (60%)... Procesando datos",
"analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos",
"analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura",
"analyzing_processing_subtitles_ytdlp": "Analizando (90%)... Procesando subtítulos",
"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"
},
"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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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"
},
"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"
},
"tabs": {
"cookies": "Se connecter avec des cookies",
"custom_command": "Commande personnalisée",
"proxy": "Proxy",
"language": "Langue"
},
"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",
"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)"
},
"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}"
},
"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é"
},
"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...",
"updating_pip": "🐍 Mise à jour du yt-dlp système via pip...",
"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}",
"checking_pip": "🔍 Vérification de l'installation pip actuelle...",
"current_version": "📋 Version actuelle : {version}",
"not_found_pip": "⚠️ yt-dlp non trouvé via pip, tentative d'installation...",
"checking_latest": "🌐 Vérification de la dernière version...",
"failed_check_updates": "❌ Échec de la vérification des mises à jour",
"latest_version": "🆕 Dernière version : {version}",
"updating_from_to": "⬆️ Mise à jour de {current} vers {latest}...",
"running_pip_install": "📦 Exécution de pip install --upgrade...",
"pip_completed": "✅ Mise à jour pip terminée avec succès !",
"pip_failed": "❌ Échec de la mise à jour pip : {error}",
"already_up_to_date": "✅ yt-dlp est déjà à jour !",
"pip_timeout": "❌ Délai d'attente de mise à jour pip dépassé après 5 minutes",
"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}",
"error_pip_update": "❌ Erreur pendant la mise à jour pip : {error}",
"pip_update_failed": "❌ La mise à jour pip a échoué : {error}"
},
"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": "🔄",
"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",
"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": "Vérifier les mises à jour maintenant",
"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\nUtilisez le bouton 'Mettre à jour yt-dlp' dans la fenêtre principale pour mettre à 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}",
"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}"
},
"main_ui": {
"url_placeholder": "Entrer l'URL de la vidéo ou de la playlist YouTube",
"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",
"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": "Analyse (0%)... Préparation de la requête",
"analyzing_extracting_basic": "Analyse (15%)... Extraction des informations de base",
"analyzing_extracting_detailed": "Analyse (30%)... Extraction des informations détaillées",
"analyzing_processing_video": "Analyse (45%)... Traitement des données vidéo",
"analyzing_processing_formats": "Analyse (60%)... Traitement des formats",
"analyzing_loading_thumbnail": "Analyse (75%)... Chargement de la miniature",
"analyzing_processing_subtitles": "Analyse (85%)... Traitement des sous-titres",
"analyzing_updating_table": "Analyse (95%)... Mise à jour du tableau des formats",
"analysis_complete": "Analyse terminée !",
"analyzing_extracting_ytdlp": "Analyse (30%)... Extraction d'informations avec l'exécutable yt-dlp",
"analyzing_processing_data": "Analyse (60%)... Traitement des données",
"analyzing_processing_formats_ytdlp": "Analyse (75%)... Traitement des formats",
"analyzing_loading_thumbnail_ytdlp": "Analyse (85%)... Chargement de la miniature",
"analyzing_processing_subtitles_ytdlp": "Analyse (90%)... Traitement des sous-titres",
"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é"
},
"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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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": "फ़ोल्डर स्थान खोलें"
},
"dialogs": {
"custom_options": "कस्टम विकल्प",
"settings": "सेटिंग्स",
"select_folder": "डाउनलोड फ़ोल्डर चुनें",
"sponsorblock_categories": "SponsorBlock श्रेणियां",
"sponsorblock_description": "डाउनलोड के दौरान अपने आप हटाए जाने वाले वीडियो सेगमेंट के प्रकार चुनें।\nSponsorBlock इन सेगमेंट की पहचान के लिए समुदाय द्वारा प्रस्तुत डेटा का उपयोग करता है।",
"select_subtitles": "उपशीर्षक चुनें",
"filter_languages_placeholder": "भाषाएं फ़िल्टर करें (जैसे: hi, en)...",
"no_subtitles_available": "कोई उपशीर्षक उपलब्ध नहीं",
"matching": "मेल खाता"
},
"tabs": {
"cookies": "कुकीज़ के साथ लॉगिन",
"custom_command": "कस्टम कमांड",
"proxy": "प्रॉक्सी",
"language": "भाषा"
},
"cookies": {
"help_text": "प्रमाणीकरण के लिए कुकीज़ प्रदान करने का तरीका चुनें।\nयह निजी वीडियो और उच्च गुणवत्ता वाली ऑडियो फ़ाइलें डाउनलोड करने की अनुमति देता है।",
"cookie_source": "कुकी स्रोत",
"use_cookie_file": "कुकी फ़ाइल का उपयोग करें",
"extract_from_browser": "ब्राउज़र से निकालें",
"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)"
},
"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}"
},
"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": "भू-प्रॉक्सी कॉन्फ़िगर की गई"
},
"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 बाइनरी अपडेट हो रही है...",
"updating_pip": "🐍 pip के माध्यम से सिस्टम yt-dlp अपडेट हो रहा है...",
"update_failed": "❌ yt-dlp अपडेट असफल। कृपया पुनः प्रयास करें या अपना इंटरनेट कनेक्शन जांचें।",
"binary_updated": "✅ बाइनरी सफलतापूर्वक अपडेट हुई!",
"update_failed_stderr": "❌ yt-dlp अपडेट असफल: {error}",
"update_timeout": "❌ yt-dlp अपडेट टाइमआउट।",
"unexpected_error": "❌ अपडेट के दौरान अनपेक्षित त्रुटि: {error}",
"checking_pip": "🔍 वर्तमान pip इंस्टॉलेशन की जांच हो रही है...",
"current_version": "📋 वर्तमान संस्करण: {version}",
"not_found_pip": "⚠️ pip के माध्यम से yt-dlp नहीं मिला, इंस्टॉल करने का प्रयास हो रहा है...",
"checking_latest": "🌐 नवीनतम संस्करण की जांच हो रही है...",
"failed_check_updates": "❌ अपडेट जांच असफल",
"latest_version": "🆕 नवीनतम संस्करण: {version}",
"updating_from_to": "⬆️ {current} से {latest} पर अपडेट हो रहा है...",
"running_pip_install": "📦 pip install --upgrade चल रहा है...",
"pip_completed": "✅ Pip अपडेट सफलतापूर्वक पूर्ण!",
"pip_failed": "❌ Pip अपडेट असफल: {error}",
"already_up_to_date": "✅ yt-dlp पहले से अप टू डेट है!",
"pip_timeout": "❌ pip अपडेट 5 मिनट के बाद समय समाप्त हो गया",
"update_success": "✅ yt-dlp सफलतापूर्वक अपडेट किया गया है!",
"already_latest": "yt-dlp अप टू डेट है (संस्करण {version})",
"network_error": "❌ अपडेट के दौरान नेटवर्क त्रुटि: {error}",
"general_error": "❌ अपडेट असफल: {error}",
"error_pip_update": "❌ pip अपडेट के दौरान त्रुटि: {error}",
"pip_update_failed": "❌ Pip अपडेट असफल: {error}"
},
"about": {
"title": "YTSage के बारे में",
"version": "संस्करण {version}",
"description": "साफ PySide6 इंटरफेस के साथ आधुनिक YouTube डाउनलोडर।",
"author": "द्वारा: {author}",
"github": "GitHub: {repo}",
"system_info": "सिस्टम जानकारी",
"loading": "🔄 सिस्टम जानकारी लोड हो रही है...",
"refresh": "🔄",
"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": "कोई नहीं",
"auto_update_ytdlp": "yt-dlp स्वचालित अपडेट",
"enable_auto_updates": "yt-dlp स्वचालित अपडेट सक्षम करें",
"update_frequency": "अपडेट आवृत्ति:",
"check_startup": "हर स्टार्टअप पर जांचें (जांच के बीच कम से कम 1 घंटा)",
"check_daily": "दैनिक जांचें",
"check_weekly": "साप्ताहिक जांचें",
"check_updates_now": "अभी अपडेट की जांच करें",
"update_check_title": "अपडेट जांच",
"could_not_determine_version": "वर्तमान yt-dlp संस्करण निर्धारित नहीं हो सका।",
"update_available_dialog": "अपडेट उपलब्ध!\n\nवर्तमान: {current}\nनवीनतम: {latest}\n\nअपडेट करने के लिए मुख्य विंडो में 'yt-dlp अपडेट करें' बटन का उपयोग करें।",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "YouTube वीडियो या प्लेलिस्ट URL दर्ज करें",
"merge_subtitles": "उपशीर्षक मर्ज करें",
"save_thumbnail": "थंबनेल सेव करें",
"save_description": "विवरण सेव करें",
"embed_chapters": "चैप्टर एम्बेड करें",
"subtitles_selected": "{count} चुना गया",
"all_selected": "सभी चुने गए",
"select_videos_all": "वीडियो चुनें... (सभी चुने गए)",
"please_enter_url": "कृपया पहले URL दर्ज करें",
"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": "विश्लेषण (0%)... अनुरोध तैयार हो रहा है",
"analyzing_extracting_basic": "विश्लेषण (15%)... बुनियादी जानकारी निकाली जा रही है",
"analyzing_extracting_detailed": "विश्लेषण (30%)... विस्तृत जानकारी निकाली जा रही है",
"analyzing_processing_video": "विश्लेषण (45%)... वीडियो डेटा प्रोसेस हो रहा है",
"analyzing_processing_formats": "विश्लेषण (60%)... प्रारूप प्रोसेस हो रहे हैं",
"analyzing_loading_thumbnail": "विश्लेषण (75%)... थंबनेल लोड हो रहा है",
"analyzing_processing_subtitles": "विश्लेषण (85%)... उपशीर्षक प्रोसेस हो रहे हैं",
"analyzing_updating_table": "विश्लेषण (95%)... प्रारूप तालिका अपडेट हो रही है",
"analysis_complete": "विश्लेषण पूर्ण!",
"analyzing_extracting_ytdlp": "विश्लेषण (30%)... yt-dlp executable के साथ जानकारी निकाली जा रही है",
"analyzing_processing_data": "विश्लेषण (60%)... डेटा प्रोसेस हो रहा है",
"analyzing_processing_formats_ytdlp": "विश्लेषण (75%)... प्रारूप प्रोसेस हो रहे हैं",
"analyzing_loading_thumbnail_ytdlp": "विश्लेषण (85%)... थंबनेल लोड हो रहा है",
"analyzing_processing_subtitles_ytdlp": "विश्लेषण (90%)... उपशीर्षक प्रोसेस हो रहे हैं",
"select_subtitles": "उपशीर्षक चुनें...",
"sponsorblock_categories": "SponsorBlock श्रेणियां...",
"invalid_url_or_enter": "अमान्य URL या कृपया URL दर्ज करें।",
"zero_selected": "0 चयनित"
},
"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": "डाउनलोड रद्द हो रहा है..."
},
"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}"
},
"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": "प्लेलिस्ट वीडियो चुनें"
},
"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": "🔍 अभी अपडेट की जांच करें"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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"
},
"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"
},
"tabs": {
"cookies": "Masuk dengan cookies",
"custom_command": "Perintah khusus",
"proxy": "Proxy",
"language": "Bahasa"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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...",
"updating_pip": "🐍 Memperbarui yt-dlp sistem melalui pip...",
"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}",
"checking_pip": "🔍 Memeriksa instalasi pip saat ini...",
"current_version": "📋 Versi saat ini: {version}",
"not_found_pip": "⚠️ yt-dlp tidak ditemukan melalui pip, mencoba menginstal...",
"checking_latest": "🌐 Memeriksa versi terbaru...",
"failed_check_updates": "❌ Gagal memeriksa pembaruan",
"latest_version": "🆕 Versi terbaru: {version}",
"updating_from_to": "⬆️ Memperbarui dari {current} ke {latest}...",
"running_pip_install": "📦 Menjalankan pip install --upgrade...",
"pip_completed": "✅ Pembaruan pip berhasil diselesaikan!",
"pip_failed": "❌ Pembaruan pip gagal: {error}",
"already_up_to_date": "✅ yt-dlp sudah terbaru!",
"pip_timeout": "❌ Waktu habis untuk pembaruan pip setelah 5 menit",
"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}",
"error_pip_update": "❌ Kesalahan selama pembaruan pip: {error}",
"pip_update_failed": "❌ Pembaruan pip gagal: {error}"
},
"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": "🔄",
"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",
"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": "Periksa pembaruan sekarang",
"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\nGunakan tombol 'Perbarui yt-dlp' di jendela utama untuk memperbarui.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "Masukkan URL video atau playlist YouTube",
"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",
"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": "Menganalisis (0%)... Mempersiapkan permintaan",
"analyzing_extracting_basic": "Menganalisis (15%)... Mengekstrak informasi dasar",
"analyzing_extracting_detailed": "Menganalisis (30%)... Mengekstrak informasi detail",
"analyzing_processing_video": "Menganalisis (45%)... Memproses data video",
"analyzing_processing_formats": "Menganalisis (60%)... Memproses format",
"analyzing_loading_thumbnail": "Menganalisis (75%)... Memuat thumbnail",
"analyzing_processing_subtitles": "Menganalisis (85%)... Memproses subtitle",
"analyzing_updating_table": "Menganalisis (95%)... Memperbarui tabel format",
"analysis_complete": "Analisis selesai!",
"analyzing_extracting_ytdlp": "Menganalisis (30%)... Mengekstrak informasi dengan executable yt-dlp",
"analyzing_processing_data": "Menganalisis (60%)... Memproses data",
"analyzing_processing_formats_ytdlp": "Menganalisis (75%)... Memproses format",
"analyzing_loading_thumbnail_ytdlp": "Menganalisis (85%)... Memuat thumbnail",
"analyzing_processing_subtitles_ytdlp": "Menganalisis (90%)... Memproses subtitle",
"select_subtitles": "Pilih subtitle...",
"sponsorblock_categories": "Kategori SponsorBlock...",
"invalid_url_or_enter": "URL tidak valid atau silakan masukkan URL.",
"zero_selected": "0 dipilih"
},
"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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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"
},
"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"
},
"tabs": {
"cookies": "Accedi con i cookie",
"custom_command": "Comando personalizzato",
"proxy": "Proxy",
"language": "Lingua"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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...",
"updating_pip": "🐍 Aggiornamento yt-dlp di sistema via pip...",
"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}",
"checking_pip": "🔍 Controllo installazione pip corrente...",
"current_version": "📋 Versione corrente: {version}",
"not_found_pip": "⚠️ yt-dlp non trovato via pip, tentativo di installazione...",
"checking_latest": "🌐 Controllo ultima versione...",
"failed_check_updates": "❌ Controllo aggiornamenti fallito",
"latest_version": "🆕 Ultima versione: {version}",
"updating_from_to": "⬆️ Aggiornamento da {current} a {latest}...",
"running_pip_install": "📦 Esecuzione pip install --upgrade...",
"pip_completed": "✅ Aggiornamento pip completato con successo!",
"pip_failed": "❌ Aggiornamento pip fallito: {error}",
"already_up_to_date": "✅ yt-dlp è già aggiornato!",
"pip_timeout": "❌ Timeout aggiornamento pip dopo 5 minuti",
"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}",
"error_pip_update": "❌ Errore durante l'aggiornamento pip: {error}",
"pip_update_failed": "❌ Aggiornamento pip fallito: {error}"
},
"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": "🔄",
"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",
"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": "Controlla aggiornamenti ora",
"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\nUsa il pulsante 'Aggiorna yt-dlp' nella finestra principale per aggiornare.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "Inserisci URL video YouTube o playlist",
"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",
"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": "Analisi (0%)... Preparazione richiesta",
"analyzing_extracting_basic": "Analisi (15%)... Estrazione informazioni base",
"analyzing_extracting_detailed": "Analisi (30%)... Estrazione informazioni dettagliate",
"analyzing_processing_video": "Analisi (45%)... Elaborazione dati video",
"analyzing_processing_formats": "Analisi (60%)... Elaborazione formati",
"analyzing_loading_thumbnail": "Analisi (75%)... Caricamento miniatura",
"analyzing_processing_subtitles": "Analisi (85%)... Elaborazione sottotitoli",
"analyzing_updating_table": "Analisi (95%)... Aggiornamento tabella formati",
"analysis_complete": "Analisi completata!",
"analyzing_extracting_ytdlp": "Analisi (30%)... Estrazione informazioni usando eseguibile yt-dlp",
"analyzing_processing_data": "Analisi (60%)... Elaborazione dati",
"analyzing_processing_formats_ytdlp": "Analisi (75%)... Elaborazione formati",
"analyzing_loading_thumbnail_ytdlp": "Analisi (85%)... Caricamento miniatura",
"analyzing_processing_subtitles_ytdlp": "Analisi (90%)... Elaborazione sottotitoli",
"select_subtitles": "Seleziona sottotitoli...",
"sponsorblock_categories": "Categorie SponsorBlock...",
"invalid_url_or_enter": "URL non valido o inserisci un URL.",
"zero_selected": "0 selezionati"
},
"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 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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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": "フォルダの場所を開く"
},
"dialogs": {
"custom_options": "カスタムオプション",
"settings": "設定",
"select_folder": "ダウンロードフォルダを選択",
"sponsorblock_categories": "SponsorBlockカテゴリ",
"sponsorblock_description": "ダウンロード時に自動的に削除する動画セグメントの種類を選択してください。\nSponsorBlockはコミュニティ提供のデータを使用してこれらのセグメントを識別します。",
"select_subtitles": "字幕を選択",
"filter_languages_placeholder": "言語でフィルタ (例: ja, en)...",
"no_subtitles_available": "利用可能な字幕がありません",
"matching": "一致"
},
"tabs": {
"cookies": "Cookieでログイン",
"custom_command": "カスタムコマンド",
"proxy": "プロキシ",
"language": "言語"
},
"cookies": {
"help_text": "認証用のCookieを提供する方法を選択してください。\nこれにより、プライベート動画や高品質の音声ファイルをダウンロードできます。",
"cookie_source": "Cookieソース",
"use_cookie_file": "Cookieファイルを使用",
"extract_from_browser": "ブラウザから抽出",
"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)"
},
"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}"
},
"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": "地域プロキシが設定されました"
},
"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バイナリを更新中...",
"updating_pip": "🐍 pipでシステムyt-dlpを更新中...",
"update_failed": "❌ yt-dlpの更新に失敗しました。もう一度試すか、インターネット接続を確認してください。",
"binary_updated": "✅ バイナリが正常に更新されました!",
"update_failed_stderr": "❌ yt-dlpの更新に失敗しました: {error}",
"update_timeout": "❌ yt-dlp更新がタイムアウトしました。",
"unexpected_error": "❌ 更新中に予期しないエラーが発生しました: {error}",
"checking_pip": "🔍 現在のpipインストールを確認中...",
"current_version": "📋 現在のバージョン: {version}",
"not_found_pip": "⚠️ pipでyt-dlpが見つかりません、インストールを試みています...",
"checking_latest": "🌐 最新バージョンを確認中...",
"failed_check_updates": "❌ アップデートの確認に失敗しました",
"latest_version": "🆕 最新バージョン: {version}",
"updating_from_to": "⬆️ {current}から{latest}に更新中...",
"running_pip_install": "📦 pip install --upgradeを実行中...",
"pip_completed": "✅ pipの更新が正常に完了しました!",
"pip_failed": "❌ pipの更新に失敗しました: {error}",
"already_up_to_date": "✅ yt-dlpはすでに最新です!",
"pip_timeout": "❌ pip更新が5分後にタイムアウトしました",
"update_success": "✅ yt-dlpが正常に更新されました!",
"already_latest": "yt-dlpは最新です(バージョン {version}",
"network_error": "❌ 更新中にネットワークエラーが発生しました: {error}",
"general_error": "❌ 更新に失敗しました: {error}",
"error_pip_update": "❌ pipの更新中にエラーが発生しました: {error}",
"pip_update_failed": "❌ pipの更新に失敗しました: {error}"
},
"about": {
"title": "YTSageについて",
"version": "バージョン {version}",
"description": "クリーンなPySide6インターフェースを備えたモダンなYouTubeダウンローダー。",
"author": "作成者: {author}",
"github": "GitHub: {repo}",
"system_info": "システム情報",
"loading": "🔄 システム情報を読み込み中...",
"refresh": "🔄",
"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": "なし",
"auto_update_ytdlp": "yt-dlp自動更新",
"enable_auto_updates": "yt-dlp自動更新を有効化",
"update_frequency": "更新頻度:",
"check_startup": "起動時に毎回確認 (チェック間隔は最低1時間)",
"check_daily": "毎日確認",
"check_weekly": "毎週確認",
"check_updates_now": "今すぐ更新を確認",
"update_check_title": "更新確認",
"could_not_determine_version": "yt-dlpの現在のバージョンを確認できませんでした。",
"update_available_dialog": "アップデートが利用可能です!\n\n現在: {current}\n最新: {latest}\n\nメインウィンドウの「yt-dlpを更新」ボタンを使用して更新してください。",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "YouTubeの動画URLまたはプレイリストURLを入力",
"merge_subtitles": "字幕を結合",
"save_thumbnail": "サムネイルを保存",
"save_description": "説明を保存",
"embed_chapters": "チャプターを埋め込む",
"subtitles_selected": "{count}個選択",
"all_selected": "すべて選択済み",
"select_videos_all": "動画を選択... (すべて選択済み)",
"please_enter_url": "最初にURLを入力してください",
"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": "解析中 (0%)... リクエストを準備中",
"analyzing_extracting_basic": "解析中 (15%)... 基本情報を抽出中",
"analyzing_extracting_detailed": "解析中 (30%)... 詳細情報を抽出中",
"analyzing_processing_video": "解析中 (45%)... 動画データを処理中",
"analyzing_processing_formats": "解析中 (60%)... フォーマットを処理中",
"analyzing_loading_thumbnail": "解析中 (75%)... サムネイルを読み込み中",
"analyzing_processing_subtitles": "解析中 (85%)... 字幕を処理中",
"analyzing_updating_table": "解析中 (95%)... フォーマットテーブルを更新中",
"analysis_complete": "解析完了!",
"analyzing_extracting_ytdlp": "解析中 (30%)... yt-dlp実行ファイルを使用して情報を抽出中",
"analyzing_processing_data": "解析中 (60%)... データを処理中",
"analyzing_processing_formats_ytdlp": "解析中 (75%)... フォーマットを処理中",
"analyzing_loading_thumbnail_ytdlp": "解析中 (85%)... サムネイルを読み込み中",
"analyzing_processing_subtitles_ytdlp": "解析中 (90%)... 字幕を処理中",
"select_subtitles": "字幕を選択...",
"sponsorblock_categories": "SponsorBlockカテゴリ...",
"invalid_url_or_enter": "無効なURLまたはURLを入力してください。",
"zero_selected": "0個選択"
},
"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": "ダウンロードをキャンセル中..."
},
"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}"
},
"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": "プレイリスト動画を選択"
},
"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": "🔍 今すぐ更新を確認"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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"
},
"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"
},
"tabs": {
"cookies": "Zaloguj za pomocą ciasteczek",
"custom_command": "Polecenie niestandardowe",
"proxy": "Proxy",
"language": "Język"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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ę...",
"updating_pip": "🐍 Aktualizowanie systemowego yt-dlp przez pip...",
"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}",
"checking_pip": "🔍 Sprawdzanie aktualnej instalacji pip...",
"current_version": "📋 Aktualna wersja: {version}",
"not_found_pip": "⚠️ yt-dlp nie znaleziony przez pip, próba instalacji...",
"checking_latest": "🌐 Sprawdzanie najnowszej wersji...",
"failed_check_updates": "❌ Sprawdzanie aktualizacji nie powiodło się",
"latest_version": "🆕 Najnowsza wersja: {version}",
"updating_from_to": "⬆️ Aktualizacja z {current} do {latest}...",
"running_pip_install": "📦 Uruchamianie pip install --upgrade...",
"pip_completed": "✅ Aktualizacja pip zakończona pomyślnie!",
"pip_failed": "❌ Aktualizacja pip nie powiodła się: {error}",
"already_up_to_date": "✅ yt-dlp jest już aktualny!",
"pip_timeout": "❌ Przekroczono limit czasu aktualizacji pip po 5 minutach",
"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}",
"error_pip_update": "❌ Błąd podczas aktualizacji pip: {error}",
"pip_update_failed": "❌ Aktualizacja pip nie powiodła się: {error}"
},
"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": "🔄",
"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",
"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": "Sprawdź aktualizacje teraz",
"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\nUżyj przycisku 'Zaktualizuj yt-dlp' w głównym oknie, aby zaktualizować.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "Wprowadź URL wideo YouTube lub playlisty",
"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",
"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": "Analizowanie (0%)... Przygotowywanie żądania",
"analyzing_extracting_basic": "Analizowanie (15%)... Wyodrębnianie podstawowych informacji",
"analyzing_extracting_detailed": "Analizowanie (30%)... Wyodrębnianie szczegółowych informacji",
"analyzing_processing_video": "Analizowanie (45%)... Przetwarzanie danych wideo",
"analyzing_processing_formats": "Analizowanie (60%)... Przetwarzanie formatów",
"analyzing_loading_thumbnail": "Analizowanie (75%)... Ładowanie miniatury",
"analyzing_processing_subtitles": "Analizowanie (85%)... Przetwarzanie napisów",
"analyzing_updating_table": "Analizowanie (95%)... Aktualizowanie tabeli formatów",
"analysis_complete": "Analiza zakończona!",
"analyzing_extracting_ytdlp": "Analizowanie (30%)... Wyodrębnianie informacji za pomocą pliku wykonywalnego yt-dlp",
"analyzing_processing_data": "Analizowanie (60%)... Przetwarzanie danych",
"analyzing_processing_formats_ytdlp": "Analizowanie (75%)... Przetwarzanie formatów",
"analyzing_loading_thumbnail_ytdlp": "Analizowanie (85%)... Ładowanie miniatury",
"analyzing_processing_subtitles_ytdlp": "Analizowanie (90%)... Przetwarzanie napisów",
"select_subtitles": "Wybierz napisy...",
"sponsorblock_categories": "Kategorie SponsorBlock...",
"invalid_url_or_enter": "Nieprawidłowy URL lub wprowadź URL.",
"zero_selected": "0 wybranych"
},
"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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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"
},
"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"
},
"tabs": {
"cookies": "Entrar com Cookies",
"custom_command": "Comando Personalizado",
"proxy": "Proxy",
"language": "Idioma"
},
"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",
"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)"
},
"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}"
},
"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"
},
"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...",
"updating_pip": "🐍 Atualizando yt-dlp do sistema via pip...",
"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}",
"checking_pip": "🔍 Verificando instalação pip atual...",
"current_version": "📋 Versão atual: {version}",
"not_found_pip": "⚠️ yt-dlp não encontrado via pip, tentando instalação...",
"checking_latest": "🌐 Verificando última versão...",
"failed_check_updates": "❌ Falha ao verificar atualizações",
"latest_version": "🆕 Última versão: {version}",
"updating_from_to": "⬆️ Atualizando de {current} para {latest}...",
"running_pip_install": "📦 Executando pip install --upgrade...",
"pip_completed": "✅ Atualização pip concluída com sucesso!",
"pip_failed": "❌ Atualização pip falhou: {error}",
"already_up_to_date": "✅ yt-dlp já está atualizado!",
"pip_timeout": "❌ Tempo limite de atualização do pip esgotado após 5 minutos",
"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}",
"error_pip_update": "❌ Erro durante a atualização do pip: {error}",
"pip_update_failed": "❌ Atualização do pip falhou: {error}"
},
"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": "🔄",
"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",
"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": "Verificar Atualizações Agora",
"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\nUse o botão 'Atualizar yt-dlp' na janela principal para atualizar.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "Digite a URL do vídeo ou playlist do YouTube",
"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",
"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": "Analisando (0%)... Preparando solicitação",
"analyzing_extracting_basic": "Analisando (15%)... Extraindo informações básicas",
"analyzing_extracting_detailed": "Analisando (30%)... Extraindo informações detalhadas",
"analyzing_processing_video": "Analisando (45%)... Processando dados do vídeo",
"analyzing_processing_formats": "Analisando (60%)... Processando formatos",
"analyzing_loading_thumbnail": "Analisando (75%)... Carregando miniatura",
"analyzing_processing_subtitles": "Analisando (85%)... Processando legendas",
"analyzing_updating_table": "Analisando (95%)... Atualizando tabela de formatos",
"analysis_complete": "Análise completa!",
"analyzing_extracting_ytdlp": "Analisando (30%)... Extraindo informações com executável yt-dlp",
"analyzing_processing_data": "Analisando (60%)... Processando dados",
"analyzing_processing_formats_ytdlp": "Analisando (75%)... Processando formatos",
"analyzing_loading_thumbnail_ytdlp": "Analisando (85%)... Carregando miniatura",
"analyzing_processing_subtitles_ytdlp": "Analisando (90%)... Processando legendas",
"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"
},
"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 upload",
"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..."
},
"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}"
},
"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"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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": "Открыть расположение папки"
},
"dialogs": {
"custom_options": "Пользовательские опции",
"settings": "Настройки",
"select_folder": "Выбрать папку загрузки",
"sponsorblock_categories": "Категории SponsorBlock",
"sponsorblock_description": "Выберите типы сегментов видео, которые будут автоматически удалены во время загрузки.\nSponsorBlock использует данные, отправленные сообществом, для идентификации этих сегментов.",
"select_subtitles": "Выбрать субтитры",
"filter_languages_placeholder": "Фильтр языков (например, en, ru)...",
"no_subtitles_available": "Субтитры недоступны",
"matching": "соответствующие"
},
"tabs": {
"cookies": "Войти через Cookie",
"custom_command": "Пользовательская команда",
"proxy": "Прокси",
"language": "Язык"
},
"cookies": {
"help_text": "Выберите способ предоставления cookie для входа в систему.\nЭто позволяет загружать приватные видео и аудио премиум качества.",
"cookie_source": "Источник Cookie",
"use_cookie_file": "Использовать файл cookie",
"extract_from_browser": "Извлечь из браузера",
"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)"
},
"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}"
},
"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": "Гео-прокси настроен"
},
"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, управляемого приложением...",
"updating_pip": "🐍 Обновление системного yt-dlp через pip...",
"update_failed": "❌ Не удалось обновить yt-dlp. Попробуйте еще раз или проверьте подключение к интернету.",
"binary_updated": "✅ Бинарный файл успешно обновлен!",
"update_failed_stderr": "❌ Обновление yt-dlp не удалось: {error}",
"update_timeout": "❌ Время ожидания обновления yt-dlp истекло.",
"unexpected_error": "❌ Неожиданная ошибка во время обновления: {error}",
"checking_pip": "🔍 Проверка текущей установки pip...",
"current_version": "📋 Текущая версия: {version}",
"not_found_pip": "⚠️ yt-dlp не найден через pip, попытка установки...",
"checking_latest": "🌐 Проверка последней версии...",
"failed_check_updates": "❌ Не удалось проверить обновления",
"latest_version": "🆕 Последняя версия: {version}",
"updating_from_to": "⬆️ Обновление с {current} до {latest}...",
"running_pip_install": "📦 Выполнение pip install --upgrade...",
"pip_completed": "✅ Обновление pip завершено успешно!",
"pip_failed": "❌ Обновление pip не удалось: {error}",
"already_up_to_date": "✅ yt-dlp уже актуален!",
"pip_timeout": "❌ Время ожидания обновления pip истекло через 5 минут",
"update_success": "✅ yt-dlp был успешно обновлен!",
"already_latest": "yt-dlp актуален (версия {version})",
"network_error": "❌ Ошибка сети во время обновления: {error}",
"general_error": "❌ Обновление не удалось: {error}",
"error_pip_update": "❌ Ошибка во время обновления pip: {error}",
"pip_update_failed": "❌ Обновление pip не удалось: {error}"
},
"about": {
"title": "О YTSage",
"version": "Версия {version}",
"description": "Современный загрузчик YouTube с чистым интерфейсом PySide6.",
"author": "Автор: {author}",
"github": "GitHub: {repo}",
"system_info": "Системная информация",
"loading": "🔄 Загрузка системной информации...",
"refresh": "🔄",
"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": "Нет",
"auto_update_ytdlp": "Автообновление yt-dlp",
"enable_auto_updates": "Включить автоматические обновления yt-dlp",
"update_frequency": "Частота обновления:",
"check_startup": "Проверять при каждом запуске (минимум 1 час между проверками)",
"check_daily": "Проверять ежедневно",
"check_weekly": "Проверять еженедельно",
"check_updates_now": "Проверить обновления сейчас",
"update_check_title": "Проверка обновлений",
"could_not_determine_version": "Не удалось определить текущую версию yt-dlp.",
"update_available_dialog": "Доступно обновление!\n\nТекущая: {current}\nПоследняя: {latest}\n\nИспользуйте кнопку 'Обновить yt-dlp' в главном окне для обновления.",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "Введите URL видео или плейлиста YouTube",
"merge_subtitles": "Объединить субтитры",
"save_thumbnail": "Сохранить миниатюру",
"save_description": "Сохранить описание",
"embed_chapters": "Встроить главы",
"subtitles_selected": "выбрано: {count}",
"all_selected": "Все выбраны",
"select_videos_all": "Выбрать видео... (Все выбраны)",
"please_enter_url": "Пожалуйста, введите URL сначала",
"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": "Анализ (0%)... Подготовка запроса",
"analyzing_extracting_basic": "Анализ (15%)... Извлечение базовой информации",
"analyzing_extracting_detailed": "Анализ (30%)... Извлечение подробной информации",
"analyzing_processing_video": "Анализ (45%)... Обработка данных видео",
"analyzing_processing_formats": "Анализ (60%)... Обработка форматов",
"analyzing_loading_thumbnail": "Анализ (75%)... Загрузка миниатюры",
"analyzing_processing_subtitles": "Анализ (85%)... Обработка субтитров",
"analyzing_updating_table": "Анализ (95%)... Обновление таблицы форматов",
"analysis_complete": "Анализ завершен!",
"analyzing_extracting_ytdlp": "Анализ (30%)... Извлечение информации с исполняемым файлом yt-dlp",
"analyzing_processing_data": "Анализ (60%)... Обработка данных",
"analyzing_processing_formats_ytdlp": "Анализ (75%)... Обработка форматов",
"analyzing_loading_thumbnail_ytdlp": "Анализ (85%)... Загрузка миниатюры",
"analyzing_processing_subtitles_ytdlp": "Анализ (90%)... Обработка субтитров",
"select_subtitles": "Выбрать субтитры...",
"sponsorblock_categories": "Категории SponsorBlock...",
"invalid_url_or_enter": "Неверный URL или пожалуйста введите URL.",
"zero_selected": "0 выбрано"
},
"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": "Отмена загрузки..."
},
"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}"
},
"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": "Выбрать Видео из Плейлиста"
},
"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": "🔍 Проверить обновления сейчас"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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ç"
},
"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"
},
"tabs": {
"cookies": "Çerezlerle giriş yap",
"custom_command": "Özel komut",
"proxy": "Proxy",
"language": "Dil"
},
"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",
"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)"
},
"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ırma hatası: {error}"
},
"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ı"
},
"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...",
"updating_pip": "🐍 pip aracılığıyla sistem yt-dlp'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}",
"checking_pip": "🔍 Mevcut pip kurulumu kontrol ediliyor...",
"current_version": "📋 Mevcut sürüm: {version}",
"not_found_pip": "⚠️ yt-dlp pip aracılığıyla bulunamadı, kurulum deneniyor...",
"checking_latest": "🌐 En son sürüm kontrol ediliyor...",
"failed_check_updates": "❌ Güncelleme kontrolü başarısız",
"latest_version": "🆕 En son sürüm: {version}",
"updating_from_to": "⬆️ {current}'den {latest}'e güncelleniyor...",
"running_pip_install": "📦 pip install --upgrade çalıştırılıyor...",
"pip_completed": "✅ Pip güncellemesi başarıyla tamamlandı!",
"pip_failed": "❌ Pip güncellemesi başarısız: {error}",
"already_up_to_date": "✅ yt-dlp zaten güncel!",
"pip_timeout": "❌ pip güncellemesi 5 dakika sonra zaman aşımına uğradı",
"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}",
"error_pip_update": "❌ Pip güncellemesi sırasında hata: {error}",
"pip_update_failed": "❌ Pip güncellemesi başarısız: {error}"
},
"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": "🔄",
"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",
"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": "Şimdi güncellemeleri kontrol et",
"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üncellemek için ana penceredeki 'yt-dlp'yi güncelle' düğmesini kullanı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}",
"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}"
},
"main_ui": {
"url_placeholder": "YouTube video veya oynatma listesi URL'sini 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",
"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": "Analiz ediliyor (0%)... İstek hazırlanıyor",
"analyzing_extracting_basic": "Analiz ediliyor (15%)... Temel bilgiler çıkarılıyor",
"analyzing_extracting_detailed": "Analiz ediliyor (30%)... Ayrıntılı bilgiler çıkarılıyor",
"analyzing_processing_video": "Analiz ediliyor (45%)... Video verisi işleniyor",
"analyzing_processing_formats": "Analiz ediliyor (60%)... Formatlar işleniyor",
"analyzing_loading_thumbnail": "Analiz ediliyor (75%)... Küçük resim yükleniyor",
"analyzing_processing_subtitles": "Analiz ediliyor (85%)... Altyazılar işleniyor",
"analyzing_updating_table": "Analiz ediliyor (95%)... Format tablosu güncelleniyor",
"analysis_complete": "Analiz tamamlandı!",
"analyzing_extracting_ytdlp": "Analiz ediliyor (30%)... yt-dlp çalıştırılabiliri ile bilgiler çıkarılıyor",
"analyzing_processing_data": "Analiz ediliyor (60%)... Veriler işleniyor",
"analyzing_processing_formats_ytdlp": "Analiz ediliyor (75%)... Formatlar işleniyor",
"analyzing_loading_thumbnail_ytdlp": "Analiz ediliyor (85%)... Küçük resim yükleniyor",
"analyzing_processing_subtitles_ytdlp": "Analiz ediliyor (90%)... Altyazılar işleniyor",
"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"
},
"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ükleme 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..."
},
"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}"
},
"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ç"
},
"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"
}
}
+416
View File
@@ -0,0 +1,416 @@
{
"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": {
"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": "打开文件夹位置"
},
"dialogs": {
"custom_options": "自定义选项",
"settings": "设置",
"select_folder": "选择下载文件夹",
"sponsorblock_categories": "SponsorBlock 分类",
"sponsorblock_description": "选择在下载期间自动删除的视频片段类型。\nSponsorBlock 使用社区提交的数据来识别这些片段。",
"select_subtitles": "选择字幕",
"filter_languages_placeholder": "过滤语言(例如:en, zh...",
"no_subtitles_available": "无可用字幕",
"matching": "匹配"
},
"tabs": {
"cookies": "使用 Cookie 登录",
"custom_command": "自定义命令",
"proxy": "代理",
"language": "语言"
},
"cookies": {
"help_text": "选择如何提供 cookie 来登录。\n这允许下载私人视频和高级质量音频。",
"cookie_source": "Cookie 来源",
"use_cookie_file": "使用 cookie 文件",
"extract_from_browser": "从浏览器提取",
"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)"
},
"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}"
},
"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": "地理代理已配置"
},
"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 二进制文件...",
"updating_pip": "🐍 正在通过 pip 更新系统 yt-dlp...",
"update_failed": "❌ 更新 yt-dlp 失败。请重试或检查您的互联网连接。",
"binary_updated": "✅ 二进制文件更新成功!",
"update_failed_stderr": "❌ yt-dlp 更新失败:{error}",
"update_timeout": "❌ yt-dlp 更新超时。",
"unexpected_error": "❌ 更新期间出现意外错误:{error}",
"checking_pip": "🔍 检查当前 pip 安装...",
"current_version": "📋 当前版本:{version}",
"not_found_pip": "⚠️ 通过 pip 未找到 yt-dlp,尝试安装...",
"checking_latest": "🌐 检查最新版本...",
"failed_check_updates": "❌ 检查更新失败",
"latest_version": "🆕 最新版本:{version}",
"updating_from_to": "⬆️ 从 {current} 更新到 {latest}...",
"running_pip_install": "📦 运行 pip install --upgrade...",
"pip_completed": "✅ pip 更新完成成功!",
"pip_failed": "❌ pip 更新失败:{error}",
"pip_timeout": "❌ pip 更新超时(超过5分钟)",
"already_up_to_date": "✅ yt-dlp已是最新版本!",
"update_success": "✅ yt-dlp 已成功更新!",
"already_latest": "yt-dlp 是最新版本(版本 {version}",
"network_error": "❌ 更新期间网络错误:{error}",
"general_error": "❌ 更新失败:{error}",
"error_pip_update": "❌ pip 更新期间出错:{error}",
"pip_update_failed": "❌ pip 更新失败:{error}"
},
"about": {
"title": "关于 YTSage",
"version": "版本 {version}",
"description": "具有简洁 PySide6 界面的现代 YouTube 下载器。",
"author": "作者:{author}",
"github": "GitHub{repo}",
"system_info": "系统信息",
"loading": "🔄 正在加载系统信息...",
"refresh": "🔄",
"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": "无",
"auto_update_ytdlp": "自动更新 yt-dlp",
"enable_auto_updates": "启用 yt-dlp 自动更新",
"update_frequency": "更新频率:",
"check_startup": "每次启动时检查(检查间隔最少 1 小时)",
"check_daily": "每日检查",
"check_weekly": "每周检查",
"check_updates_now": "立即检查更新",
"update_check_title": "更新检查",
"could_not_determine_version": "无法确定当前 yt-dlp 版本。",
"update_available_dialog": "有可用更新!\\n\\n当前:{current}\\n最新:{latest}\\n\\n使用主窗口中的'更新 yt-dlp'按钮进行更新。",
"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}",
"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}"
},
"main_ui": {
"url_placeholder": "输入 YouTube 视频或播放列表网址",
"merge_subtitles": "合并字幕",
"save_thumbnail": "保存缩略图",
"save_description": "保存描述",
"embed_chapters": "嵌入章节",
"subtitles_selected": "已选择 {count} 个",
"all_selected": "全部已选择",
"select_videos_all": "选择视频...(全部已选择)",
"please_enter_url": "请先输入网址",
"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": "分析中 (0%)... 正在准备请求",
"analyzing_extracting_basic": "分析中 (15%)... 正在提取基本信息",
"analyzing_extracting_detailed": "分析中 (30%)... 正在提取详细信息",
"analyzing_processing_video": "分析中 (45%)... 正在处理视频数据",
"analyzing_processing_formats": "分析中 (60%)... 正在处理格式",
"analyzing_loading_thumbnail": "分析中 (75%)... 正在加载缩略图",
"analyzing_processing_subtitles": "分析中 (85%)... 正在处理字幕",
"analyzing_updating_table": "分析中 (95%)... 正在更新格式表",
"analysis_complete": "分析完成!",
"analyzing_extracting_ytdlp": "分析中 (30%)... 使用 yt-dlp 可执行文件提取信息",
"analyzing_processing_data": "分析中 (60%)... 正在处理数据",
"analyzing_processing_formats_ytdlp": "分析中 (75%)... 正在处理格式",
"analyzing_loading_thumbnail_ytdlp": "分析中 (85%)... 正在加载缩略图",
"analyzing_processing_subtitles_ytdlp": "分析中 (90%)... 正在处理字幕",
"select_subtitles": "选择字幕...",
"sponsorblock_categories": "SponsorBlock 分类...",
"invalid_url_or_enter": "无效的URL或请输入URL。",
"zero_selected": "已选择 0 个"
},
"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": "正在取消下载..."
},
"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}"
},
"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": "选择播放列表视频"
},
"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": "🔍 立即检查更新"
}
}
+2 -5
View File
@@ -2,11 +2,8 @@ import sys
from PySide6.QtWidgets import QApplication, QMessageBox from PySide6.QtWidgets import QApplication, QMessageBox
from src.core.ytsage_logging import logger from src.utils.ytsage_logger import logger
from src.core.ytsage_yt_dlp import ( # Import the new yt-dlp setup functions from src.core.ytsage_yt_dlp import check_ytdlp_binary, setup_ytdlp # Import the new yt-dlp setup functions
check_ytdlp_binary,
setup_ytdlp,
)
from src.gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main from src.gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main
+1 -1
View File
@@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader
A modern, user-friendly YouTube video downloader built with PySide6. A modern, user-friendly YouTube video downloader built with PySide6.
""" """
__version__ = "4.8.3" __version__ = "4.9.0b"
__author__ = "oop7" __author__ = "oop7"
+118 -64
View File
@@ -7,9 +7,13 @@ from pathlib import Path
from PySide6.QtCore import QObject, QThread, Signal from PySide6.QtCore import QObject, QThread, Signal
from src.core.ytsage_logging import logger
from src.core.ytsage_yt_dlp import get_yt_dlp_path from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS
from src.utils.ytsage_localization import LocalizationManager
from src.utils.ytsage_logger import logger
# Shorthand for localization
_ = LocalizationManager.get_text
try: try:
import yt_dlp # Keep yt_dlp import here - only downloader uses it. import yt_dlp # Keep yt_dlp import here - only downloader uses it.
@@ -38,6 +42,7 @@ class DownloadThread(QThread):
error_signal = Signal(str) error_signal = Signal(str)
file_exists_signal = Signal(str) # New signal for file existence file_exists_signal = Signal(str) # New signal for file existence
update_details = Signal(str) # New signal for filename, speed, ETA update_details = Signal(str) # New signal for filename, speed, ETA
update_details = Signal(str) # New signal for filename, speed, ETA
def __init__( def __init__(
self, self,
@@ -58,6 +63,8 @@ class DownloadThread(QThread):
rate_limit=None, rate_limit=None,
download_section=None, download_section=None,
force_keyframes=False, force_keyframes=False,
proxy_url=None,
geo_proxy_url=None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.url = url self.url = url
@@ -77,6 +84,8 @@ class DownloadThread(QThread):
self.rate_limit = rate_limit self.rate_limit = rate_limit
self.download_section = download_section self.download_section = download_section
self.force_keyframes = force_keyframes self.force_keyframes = force_keyframes
self.proxy_url = proxy_url
self.geo_proxy_url = geo_proxy_url
self.paused = False self.paused = False
self.cancelled = False self.cancelled = False
self.process = None self.process = None
@@ -94,12 +103,31 @@ class DownloadThread(QThread):
pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243. pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243.
for file_path in self.path.iterdir(): for file_path in self.path.iterdir():
if file_path.suffix == ".part" or pattern.search(file_path.name): if file_path.suffix == ".part" or pattern.search(file_path.name):
try: self._safe_delete_with_retry(file_path)
file_path.unlink(missing_ok=True)
except Exception as e:
logger.error(f"Error deleting {file_path.name}: {str(e)}")
except Exception as e: except Exception as e:
self.error_signal.emit(f"Error cleaning partial files: {str(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 = 3, delay: float = 1.0) -> None:
"""Safely delete a file with retry mechanism for Windows file locking issues"""
for attempt in range(max_retries):
try:
if file_path.exists():
file_path.unlink(missing_ok=True)
logger.info(f"Successfully deleted {file_path.name}")
return
except PermissionError as e:
if "being used by another process" in str(e) and 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 *= 1.5 # Exponential backoff
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 cleanup_subtitle_files(self) -> None: def cleanup_subtitle_files(self) -> None:
"""Delete subtitle files after they have been merged into the video file""" """Delete subtitle files after they have been merged into the video file"""
@@ -111,7 +139,7 @@ class DownloadThread(QThread):
logger.debug(f"Deleted subtitle file: {path.name}") logger.debug(f"Deleted subtitle file: {path.name}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error deleting subtitle file {path}: {e}") logger.exception(f"Error deleting subtitle file {path}: {e}")
return False return False
try: try:
@@ -130,7 +158,7 @@ class DownloadThread(QThread):
else: else:
logger.debug(f"Deleted {deleted_count[1]} of {len(new_subtitle_files)} new subtitle files") logger.debug(f"Deleted {deleted_count[1]} of {len(new_subtitle_files)} new subtitle files")
except Exception as e: except Exception as e:
logger.error(f"Error cleaning subtitle files: {str(e)}") logger.exception(f"Error cleaning subtitle files: {e}")
def check_file_exists(self) -> bool | None: def check_file_exists(self) -> bool | None:
"""Check if the file already exists before downloading""" """Check if the file already exists before downloading"""
@@ -138,18 +166,28 @@ class DownloadThread(QThread):
logger.debug("Starting file existence check") logger.debug("Starting file existence check")
# Use yt-dlp to get the filename without downloading, suppressing warnings # Use yt-dlp to get the filename without downloading, suppressing warnings
ydl_opts_check = { ydl_opts_check = {
"logger": logger, # passed app logger
"quiet": True, "quiet": True,
"skip_download": True, "skip_download": True,
"no_warnings": True, # <-- Suppress warnings during check "no_warnings": True, # <-- Suppress warnings during check
"ignoreerrors": True, # Also ignore other potential errors during this check "ignoreerrors": True, # Also ignore other potential errors during this check
"outtmpl": {"default": f"{self.path.as_posix()}/%(title)s.%(ext)s"}, "outtmpl": {"default": str(self.path / "%(title)s.%(ext)s")},
"format": (self.format_id if self.format_id else "best"), # Use selected format or best "format": (self.format_id if self.format_id else "best"), # Use selected format or best
} }
if self.cookie_file: if self.cookie_file:
ydl_opts_check["cookiefile"] = str(self.cookie_file) ydl_opts_check["cookiefile"] = str(self.cookie_file)
elif self.browser_cookies: elif self.browser_cookies:
ydl_opts_check["cookiesfrombrowser"] = (self.browser_cookies.split(':')[0], ydl_opts_check["cookiesfrombrowser"] = (
self.browser_cookies.split(':')[1] if ':' in self.browser_cookies else None) self.browser_cookies.split(":")[0],
self.browser_cookies.split(":")[1] if ":" in self.browser_cookies else None,
)
# Add proxy settings if specified
if self.proxy_url:
ydl_opts_check["proxy"] = self.proxy_url
if self.geo_proxy_url:
ydl_opts_check["geo_verification_proxy"] = self.geo_proxy_url
if YT_DLP_AVAILABLE: if YT_DLP_AVAILABLE:
with yt_dlp.YoutubeDL(ydl_opts_check) as ydl: with yt_dlp.YoutubeDL(ydl_opts_check) as ydl:
@@ -178,10 +216,7 @@ class DownloadThread(QThread):
return False # Proceed with download attempt return False # Proceed with download attempt
except Exception as e: except Exception as e:
logger.debug(f"Error checking file existence: {str(e)}") logger.exception(f"Error checking file existence: {e}")
import traceback
traceback.print_exc()
return None return None
def _build_yt_dlp_command(self) -> list: def _build_yt_dlp_command(self) -> list:
@@ -201,6 +236,7 @@ class DownloadThread(QThread):
try: try:
if YT_DLP_AVAILABLE: if YT_DLP_AVAILABLE:
ydl_opts = { ydl_opts = {
"logger": logger,
"quiet": True, "quiet": True,
"no_warnings": True, "no_warnings": True,
"skip_download": True, "skip_download": True,
@@ -214,7 +250,7 @@ class DownloadThread(QThread):
logger.debug(f"Detected audio-only format for ID: {clean_format_id}") logger.debug(f"Detected audio-only format for ID: {clean_format_id}")
break break
except Exception as e: except Exception as e:
logger.debug(f"Error checking if format is audio-only: {e}") logger.exception(f"Error checking if format is audio-only: {e}")
# For audio-only formats, don't try to merge with video # For audio-only formats, don't try to merge with video
if is_audio_format: if is_audio_format:
@@ -229,12 +265,9 @@ class DownloadThread(QThread):
try: try:
format_ext = None format_ext = None
logger.debug(f"Getting format information for format ID: {self.format_id} (using: {clean_format_id})") logger.debug(f"Getting format information for format ID: {self.format_id} (using: {clean_format_id})")
if YT_DLP_AVAILABLE: if YT_DLP_AVAILABLE:
ydl_opts = { ydl_opts = {"quiet": True, "no_warnings": True, "skip_download": True, "logger": logger}
"quiet": True,
"no_warnings": True,
"skip_download": True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(self.url, download=False) or {} info = ydl.extract_info(self.url, download=False) or {}
# Look for the clean format ID first # Look for the clean format ID first
@@ -254,7 +287,7 @@ class DownloadThread(QThread):
# Ensure output matches the selected format - only for video formats # Ensure output matches the selected format - only for video formats
cmd.extend(["--merge-output-format", format_ext]) cmd.extend(["--merge-output-format", format_ext])
except Exception as e: except Exception as e:
logger.debug(f"Error detecting format extension: {e}") logger.exception(f"Error detecting format extension: {e}")
# If we can't determine the format, don't specify merge-output-format # If we can't determine the format, don't specify merge-output-format
pass pass
else: else:
@@ -272,7 +305,7 @@ class DownloadThread(QThread):
else: else:
output_template = f"{base_path}/%(title)s_%(resolution)s.%(ext)s" output_template = f"{base_path}/%(title)s_%(resolution)s.%(ext)s"
cmd.extend(["-o", output_template]) cmd.extend(["-o", str(output_template)])
# Add common options # Add common options
cmd.append("--force-overwrites") cmd.append("--force-overwrites")
@@ -295,7 +328,7 @@ class DownloadThread(QThread):
lang_code = sub_selection.split(" - ")[0] lang_code = sub_selection.split(" - ")[0]
lang_codes.append(lang_code) lang_codes.append(lang_code)
except Exception as e: except Exception as e:
logger.warning(f"Could not parse subtitle selection '{sub_selection}': {e}") logger.exception(f"Could not parse subtitle selection '{sub_selection}': {e}")
if lang_codes: if lang_codes:
cmd.extend(["--sub-langs", ",".join(lang_codes)]) cmd.extend(["--sub-langs", ",".join(lang_codes)])
@@ -324,6 +357,13 @@ class DownloadThread(QThread):
elif self.browser_cookies: elif self.browser_cookies:
cmd.extend(["--cookies-from-browser", 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 # Add rate limit if specified
if self.rate_limit: if self.rate_limit:
cmd.extend(["-r", self.rate_limit]) cmd.extend(["-r", self.rate_limit])
@@ -366,7 +406,7 @@ class DownloadThread(QThread):
self.initial_subtitle_files.add(file) self.initial_subtitle_files.add(file)
logger.debug(f"Found {len(self.initial_subtitle_files)} existing subtitle files before download") logger.debug(f"Found {len(self.initial_subtitle_files)} existing subtitle files before download")
except Exception as e: except Exception as e:
logger.warning(f"Error scanning for initial subtitle files: {e}") logger.exception(f"Error scanning for initial subtitle files: {e}")
if self.use_direct_command: if self.use_direct_command:
# Use direct CLI command instead of Python API # Use direct CLI command instead of Python API
@@ -377,10 +417,8 @@ class DownloadThread(QThread):
except Exception as e: except Exception as e:
# Catch errors during setup # Catch errors during setup
self.error_signal.emit(f"Critical error in download thread: {str(e)}") logger.critical(f"Critical error in download thread: {e}", exc_info=True)
import traceback self.error_signal.emit(f"Critical error in download thread: {e}")
traceback.print_exc()
def _run_direct_command(self) -> None: def _run_direct_command(self) -> None:
"""Run yt-dlp as a direct command line process instead of using Python API.""" """Run yt-dlp as a direct command line process instead of using Python API."""
@@ -389,7 +427,7 @@ class DownloadThread(QThread):
cmd_str = " ".join(shlex.quote(str(arg)) for arg in cmd) cmd_str = " ".join(shlex.quote(str(arg)) for arg in cmd)
logger.debug(f"Executing command: {cmd_str}") logger.debug(f"Executing command: {cmd_str}")
self.status_signal.emit("🚀 Starting download...") self.status_signal.emit(_("download.starting"))
self.progress_signal.emit(0) self.progress_signal.emit(0)
# Start the process # Start the process
@@ -409,8 +447,18 @@ class DownloadThread(QThread):
for line in iter(self.process.stdout.readline, ""): # type: ignore for line in iter(self.process.stdout.readline, ""): # type: ignore
if self.cancelled: if self.cancelled:
self.process.terminate() self.process.terminate()
# Wait for process to actually terminate before cleaning up files
try:
self.process.wait(timeout=5) # Wait up to 5 seconds
except subprocess.TimeoutExpired:
logger.warning("Process didn't terminate gracefully, forcing kill")
self.process.kill()
self.process.wait()
# Add delay before cleanup to allow file handles to be released
time.sleep(1)
self.cleanup_partial_files() self.cleanup_partial_files()
self.status_signal.emit("Download cancelled") self.status_signal.emit(_("download.cancelled"))
return return
# Wait if paused # Wait if paused
@@ -427,20 +475,20 @@ class DownloadThread(QThread):
# return code 127 typically means command not found # return code 127 typically means command not found
if return_code == 127: if return_code == 127:
self.error_signal.emit( self.error_signal.emit(
"Error: yt-dlp executable not found. This could be due to improper installation or a PATH issue." _("errors.ytdlp_not_found_path")
) )
return return
if return_code == 0: if return_code == 0:
self.progress_signal.emit(100) self.progress_signal.emit(100)
self.status_signal.emit("✅ Download completed!") self.status_signal.emit(_("download.completed"))
# Clean up subtitle files if they were merged, with a small delay # Clean up subtitle files if they were merged, with a small delay
# to ensure the embedding process has completed # to ensure the embedding process has completed
if self.merge_subs: if self.merge_subs:
# Add a significant delay to ensure ffmpeg has released all file handles # Add a significant delay to ensure ffmpeg has released all file handles
# and any post-processing is complete # and any post-processing is complete
self.status_signal.emit("✅ Download completed! Cleaning up...") self.status_signal.emit(_("download.completed_cleaning"))
time.sleep(3) # Increased delay to 3 seconds time.sleep(3) # Increased delay to 3 seconds
self.cleanup_subtitle_files() self.cleanup_subtitle_files()
@@ -448,7 +496,7 @@ class DownloadThread(QThread):
else: else:
# Check if it was cancelled # Check if it was cancelled
if self.cancelled: if self.cancelled:
self.status_signal.emit("Download cancelled") self.status_signal.emit(_("download.cancelled"))
else: else:
# Provide more descriptive error message for possible yt-dlp conflicts # Provide more descriptive error message for possible yt-dlp conflicts
if return_code == 1: if return_code == 1:
@@ -457,10 +505,16 @@ class DownloadThread(QThread):
) )
else: else:
self.error_signal.emit(f"Download failed with return code {return_code}") self.error_signal.emit(f"Download failed with return code {return_code}")
# Add delay before cleanup to allow file handles to be released
time.sleep(1)
self.cleanup_partial_files() self.cleanup_partial_files()
except Exception as e: except Exception as e:
self.error_signal.emit(f"Error in direct command: {str(e)}") logger.exception(f"Error in direct command: {e}")
self.error_signal.emit(f"Error in direct command: {e}")
# Add delay before cleanup to allow file handles to be released
time.sleep(1)
self.cleanup_partial_files() self.cleanup_partial_files()
def _parse_output_line(self, line) -> None: def _parse_output_line(self, line) -> None:
@@ -499,31 +553,31 @@ class DownloadThread(QThread):
# Check if this is explicitly an audio stream download # Check if this is explicitly an audio stream download
if is_audio_download or "Downloading audio" in line: if is_audio_download or "Downloading audio" in line:
self.status_signal.emit(f"⏬ Downloading audio...") self.status_signal.emit(_("download.downloading_audio"))
# Video file extensions with likely video content # Video file extensions with likely video content
elif ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: elif ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
self.status_signal.emit(f"⏬ Downloading video...") self.status_signal.emit(_("download.downloading_video"))
# Audio file extensions # Audio file extensions
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
self.status_signal.emit(f"⏬ Downloading audio...") self.status_signal.emit(_("download.downloading_audio"))
# Subtitle file extensions # Subtitle file extensions
elif ext in [".vtt", ".srt", ".ass", ".ssa"]: elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
self.status_signal.emit(f"⏬ Downloading subtitle...") self.status_signal.emit(_("download.downloading_subtitle"))
# Default case # Default case
else: else:
self.status_signal.emit(f"⏬ Downloading...") self.status_signal.emit(_("download.downloading"))
except Exception as e: except Exception as e:
logger.error(f"Error extracting filename from line '{line}': {e}") logger.exception(f"Error extracting filename from line '{line}': {e}")
self.status_signal.emit("⚡ Downloading...") # Fallback status self.status_signal.emit(_("download.downloading_fallback")) # Fallback status
return # Don't process this line further for speed/ETA return # Don't process this line further for speed/ETA
# Check for specific download types in the output # Check for specific download types in the output
if "Downloading video" in line: if "Downloading video" in line:
self.status_signal.emit(f"⏬ Downloading video...") self.status_signal.emit(_("download.downloading_video"))
return return
elif "Downloading audio" in line: elif "Downloading audio" in line:
self.status_signal.emit(f"⏬ Downloading audio...") self.status_signal.emit(_("download.downloading_audio"))
return return
# Detect subtitle file creation # Detect subtitle file creation
@@ -538,7 +592,7 @@ class DownloadThread(QThread):
# Clean up the path - remove any duplicated directory paths # Clean up the path - remove any duplicated directory paths
# Sometimes yt-dlp output contains malformed paths like "dir: dir/file" # Sometimes yt-dlp output contains malformed paths like "dir: dir/file"
if ":" in subtitle_file and os.name == 'nt': # Windows paths if ":" in subtitle_file and os.name == "nt": # Windows paths
# Look for pattern like "C:\path: C:\path\file" and extract the latter # Look for pattern like "C:\path: C:\path\file" and extract the latter
colon_parts = subtitle_file.split(": ") colon_parts = subtitle_file.split(": ")
if len(colon_parts) > 1: if len(colon_parts) > 1:
@@ -546,7 +600,7 @@ class DownloadThread(QThread):
subtitle_file = colon_parts[-1].strip() subtitle_file = colon_parts[-1].strip()
# Show subtitle download message # Show subtitle download message
self.status_signal.emit(f"⏬ Downloading subtitle...") self.status_signal.emit(_("download.downloading_subtitle"))
# Store the subtitle file path for later deletion if merging is enabled # Store the subtitle file path for later deletion if merging is enabled
if self.merge_subs: if self.merge_subs:
subtitle_path = Path(subtitle_file) subtitle_path = Path(subtitle_file)
@@ -559,24 +613,24 @@ class DownloadThread(QThread):
# Send status updates based on output line content # Send status updates based on output line content
if "Downloading webpage" in line or "Extracting URL" in line: if "Downloading webpage" in line or "Extracting URL" in line:
self.status_signal.emit("🔍 Fetching video information...") self.status_signal.emit(_("download.fetching_info"))
self.progress_signal.emit(0) self.progress_signal.emit(0)
elif "Downloading API JSON" in line: elif "Downloading API JSON" in line:
self.status_signal.emit("📋 Processing playlist data...") self.status_signal.emit(_("download.processing_playlist"))
self.progress_signal.emit(0) self.progress_signal.emit(0)
elif "Downloading m3u8 information" in line: elif "Downloading m3u8 information" in line:
self.status_signal.emit("🎯 Preparing video streams...") self.status_signal.emit(_("download.preparing_streams"))
self.progress_signal.emit(0) self.progress_signal.emit(0)
elif "[download] Downloading video " in line: elif "[download] Downloading video " in line:
self.status_signal.emit("⏬ Downloading video...") self.status_signal.emit(_("download.downloading_video"))
elif "[download] Downloading audio " in line: elif "[download] Downloading audio " in line:
self.status_signal.emit("⏬ Downloading audio...") self.status_signal.emit(_("download.downloading_audio"))
elif "Downloading format" in line: elif "Downloading format" in line:
# Try to detect if it's audio or video format # Try to detect if it's audio or video format
if " - audio only" in line: if " - audio only" in line:
self.status_signal.emit("⏬ Downloading audio...") self.status_signal.emit(_("download.downloading_audio"))
elif " - video only" in line: elif " - video only" in line:
self.status_signal.emit("⏬ Downloading video...") self.status_signal.emit(_("download.downloading_video"))
else: else:
# Don't emit generic message - format is unclear # Don't emit generic message - format is unclear
pass pass
@@ -603,19 +657,19 @@ class DownloadThread(QThread):
eta_str = eta_match.group(1) if eta_match else "N/A" eta_str = eta_match.group(1) if eta_match else "N/A"
# Simplify status message to only show the speed and ETA # Simplify status message to only show the speed and ETA
status = f"Speed: {speed_str} | ETA: {eta_str}" status = f"{_('download.speed')}: {speed_str} | {_('download.eta')}: {eta_str}"
self.update_details.emit(status) self.update_details.emit(status)
except Exception as e: except Exception as e:
# If parsing fails, just show basic status (maybe log the error) # If parsing fails, just show basic status (maybe log the error)
logger.error(f"Error parsing download details line: {line} -> {e}") logger.exception(f"Error parsing download details line: {line} -> {e}")
pass # Keep basic status emission below if needed, or emit generic details pass # Keep basic status emission below if needed, or emit generic details
# Check for post-processing # Check for post-processing
if "[Merger]" in line or "Merging formats" in line: if "[Merger]" in line or "Merging formats" in line:
self.status_signal.emit("✨ Post-processing: Merging formats...") self.status_signal.emit(_("download.merging_formats"))
self.progress_signal.emit(95) self.progress_signal.emit(95)
elif "SponsorBlock" in line: elif "SponsorBlock" in line:
self.status_signal.emit("✨ Post-processing: Removing sponsor segments...") self.status_signal.emit(_("download.removing_sponsor_segments"))
self.progress_signal.emit(97) self.progress_signal.emit(97)
elif "Deleting original file" in line: elif "Deleting original file" in line:
self.progress_signal.emit(98) self.progress_signal.emit(98)
@@ -639,7 +693,7 @@ class DownloadThread(QThread):
self.file_exists_signal.emit(filename) self.file_exists_signal.emit(filename)
else: else:
logger.info(f"Could not extract filename from 'already downloaded' line: {line}") logger.info(f"Could not extract filename from 'already downloaded' line: {line}")
self.status_signal.emit("⚠️ File already exists") # Fallback status self.status_signal.emit(_("download.file_exists")) # Fallback status
elif "Finished downloading" in line: elif "Finished downloading" in line:
self.progress_signal.emit(100) self.progress_signal.emit(100)
@@ -649,18 +703,18 @@ class DownloadThread(QThread):
# Video file extensions # Video file extensions
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
self.status_signal.emit(f"✅ Video download completed!") self.status_signal.emit(_("download.video_completed"))
# Audio file extensions # Audio file extensions
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
self.status_signal.emit(f"✅ Audio download completed!") self.status_signal.emit(_("download.audio_completed"))
# Subtitle file extensions # Subtitle file extensions
elif ext in [".vtt", ".srt", ".ass", ".ssa"]: elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
self.status_signal.emit(f"✅ Subtitle download completed!") self.status_signal.emit(_("download.subtitle_completed"))
# Default case # Default case
else: else:
self.status_signal.emit("✅ Download completed!") self.status_signal.emit(_("download.completed"))
else: else:
self.status_signal.emit("✅ Download completed!") self.status_signal.emit(_("download.completed"))
self.update_details.emit("") # Clear details label on completion self.update_details.emit("") # Clear details label on completion
+21 -15
View File
@@ -7,7 +7,7 @@ from pathlib import Path
import requests import requests
from src.core.ytsage_logging import logger from src.utils.ytsage_logger import logger
from src.utils.ytsage_constants import ( from src.utils.ytsage_constants import (
FFMPEG_7Z_DOWNLOAD_URL, FFMPEG_7Z_DOWNLOAD_URL,
FFMPEG_7Z_SHA256_URL, FFMPEG_7Z_SHA256_URL,
@@ -46,7 +46,7 @@ def download_file(url, dest_path, progress_callback=None) -> bool:
progress_callback(f"⚡ Downloading FFmpeg components... {progress}%") progress_callback(f"⚡ Downloading FFmpeg components... {progress}%")
return True return True
except requests.RequestException as e: except requests.RequestException as e:
logger.info(f"Download error: {str(e)}") logger.info(f"Download error: {e}")
return False return False
@@ -80,7 +80,7 @@ def verify_sha256(file_path, expected_hash_url) -> bool:
logger.info(f"Actual: {actual_hash}") logger.info(f"Actual: {actual_hash}")
return False return False
except Exception as e: except Exception as e:
logger.info(f"⚠️ SHA-256 verification error: {str(e)}") logger.info(f"⚠️ SHA-256 verification error: {e}")
return False return False
@@ -128,7 +128,7 @@ def get_ffmpeg_path() -> str | Path:
ffmpeg_path = result.stdout.strip() ffmpeg_path = result.stdout.strip()
return ffmpeg_path return ffmpeg_path
except Exception as e: except Exception as e:
logger.error(f"Error finding ffmpeg in PATH: {e}") logger.exception(f"Error finding ffmpeg in PATH: {e}")
# If not found in PATH, check the installation directory # If not found in PATH, check the installation directory
ffmpeg_install_path = get_ffmpeg_install_path() ffmpeg_install_path = get_ffmpeg_install_path()
@@ -171,7 +171,7 @@ def check_ffmpeg_installed() -> bool:
return True return True
return False return False
except Exception as e: except Exception as e:
logger.info(f"FFmpeg check error: {str(e)}") logger.info(f"FFmpeg check error: {e}")
return False return False
@@ -219,7 +219,7 @@ def install_ffmpeg_windows() -> bool:
timeout=300, timeout=300,
) # 5-minute timeout ) # 5-minute timeout
except Exception as e: except Exception as e:
logger.error(f"7z extraction failed: {str(e)}, trying zip fallback...") logger.exception(f"7z extraction failed: {e}, trying zip fallback...")
use_7zip = False use_7zip = False
else: else:
logger.error("SHA-256 verification failed for 7z file, trying zip fallback...") logger.error("SHA-256 verification failed for 7z file, trying zip fallback...")
@@ -236,7 +236,8 @@ def install_ffmpeg_windows() -> bool:
temp_file, temp_file,
progress_callback=lambda msg: logger.debug(msg), progress_callback=lambda msg: logger.debug(msg),
): ):
raise Exception("Failed to download FFmpeg (both 7z and zip methods failed)") logger.exception("Failed to download FFmpeg (both 7z and zip methods failed)")
return False
logger.info("Extracting FFmpeg components from zip archive...") logger.info("Extracting FFmpeg components from zip archive...")
try: try:
@@ -245,7 +246,8 @@ def install_ffmpeg_windows() -> bool:
with zipfile.ZipFile(temp_file, "r") as zip_ref: with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(extract_dir) zip_ref.extractall(extract_dir)
except Exception as e: except Exception as e:
raise Exception(f"Extraction failed: {str(e)}") logger.exception(f"Extraction failed: {e}")
return False
logger.info("Configuring system paths...") logger.info("Configuring system paths...")
# Add to System Path # Add to System Path
@@ -265,13 +267,14 @@ def install_ffmpeg_windows() -> bool:
# Verify installation # Verify installation
if not check_ffmpeg_installed(): if not check_ffmpeg_installed():
raise Exception("FFmpeg installation verification failed") logger.error("FFmpeg installation verification failed")
return False
logger.info("FFmpeg installation completed successfully!") logger.info("FFmpeg installation completed successfully!")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error installing FFmpeg: {str(e)}") logger.exception(f"Error installing FFmpeg: {e}")
return False return False
@@ -298,12 +301,13 @@ def install_ffmpeg_macos() -> bool:
# Verify installation # Verify installation
if not check_ffmpeg_installed(): if not check_ffmpeg_installed():
raise Exception("FFmpeg installation verification failed") logger.error("FFmpeg installation verification failed")
return False
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error installing FFmpeg: {str(e)}") logger.exception(f"Error installing FFmpeg: {e}")
return False return False
@@ -329,16 +333,18 @@ def install_ffmpeg_linux() -> bool:
# Universal snap package # Universal snap package
subprocess.run(["sudo", "snap", "install", "ffmpeg"], check=True, timeout=300) subprocess.run(["sudo", "snap", "install", "ffmpeg"], check=True, timeout=300)
else: else:
raise Exception("No supported package manager found") logger.error("No supported package manager found")
return False
# Verify installation # Verify installation
if not check_ffmpeg_installed(): if not check_ffmpeg_installed():
raise Exception("FFmpeg installation verification failed") logger.error("FFmpeg installation verification failed")
return False
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error installing FFmpeg: {str(e)}") logger.exception(f"Error installing FFmpeg: {e}")
return False return False
-238
View File
@@ -1,238 +0,0 @@
"""
YTSage logging configuration using loguru.
This module provides centralized logging configuration for the entire YTSage application.
It replaces the inefficient print statements with structured logging using loguru.
"""
import sys
from pathlib import Path
from src.utils.ytsage_constants import APP_LOG_DIR
# Try to import loguru, but handle case where it might not be available
try:
from loguru import logger
LOGURU_AVAILABLE = True
except ImportError:
LOGURU_AVAILABLE = False
# Create a dummy logger class that does nothing
class DummyLogger:
def info(self, *args, **kwargs):
pass
def debug(self, *args, **kwargs):
pass
def warning(self, *args, **kwargs):
pass
def error(self, *args, **kwargs):
pass
def critical(self, *args, **kwargs):
pass
def remove(self, *args, **kwargs):
pass
def add(self, *args, **kwargs):
pass
def bind(self, *args, **kwargs):
return self
@property
def _core(self):
class Core:
handlers = []
return Core()
logger = DummyLogger()
def setup_logging():
"""
Configure loguru logging for YTSage application.
Sets up multiple log levels and outputs:
- Console output for INFO and above
- File output for DEBUG and above
- Separate error log file for ERROR and above
"""
if not LOGURU_AVAILABLE:
return logger
# Remove default logger to avoid duplicate output
try:
logger.remove()
except Exception:
pass
# Get the application data directory with fallbacks
try:
# logic moved to src\utils\ytsage_constants.py
log_dir = APP_LOG_DIR
except Exception:
# Ultimate fallback - use current directory
log_dir = Path.cwd() / "logs"
# Create log directory if it doesn't exist
try:
log_dir.mkdir(parents=True, exist_ok=True)
except Exception:
# If we can't create the log directory, fall back to current directory
log_dir = Path.cwd()
try:
log_dir.mkdir(exist_ok=True)
except Exception:
pass # If we still can't create it, we'll just log to console
# Console handler - INFO and above, with colors
# Check if stdout is available (it might be None in PyInstaller windowed apps)
stdout_available = sys.stdout is not None
if stdout_available:
try:
logger.add(
sys.stdout,
level="INFO",
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
colorize=True,
catch=True,
)
except Exception:
# Fallback to basic console logging without colors
try:
logger.add(
sys.stdout,
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
catch=True,
)
except Exception:
stdout_available = False
# If stdout is not available, try stderr or skip console logging entirely
if not stdout_available:
try:
if sys.stderr is not None:
logger.add(
sys.stderr,
level="WARNING", # Only warnings and errors to stderr
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
catch=True,
)
except Exception:
# If even stderr fails, we'll rely only on file logging
pass
# Only add file handlers if we successfully created a log directory
if log_dir and log_dir.exists():
try:
# Main log file - DEBUG and above, with rotation
logger.add(
log_dir / "ytsage.log",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
rotation="10 MB", # Rotate when file reaches 10MB
retention="7 days", # Keep logs for 7 days
compression="zip", # Compress old logs
catch=True,
)
# Error log file - ERROR and above only
logger.add(
log_dir / "ytsage_errors.log",
level="ERROR",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
rotation="5 MB",
retention="30 days", # Keep error logs longer
compression="zip",
catch=True,
)
except Exception as e:
# If file logging fails, just log to console
logger.warning(f"Could not set up file logging: {e}")
# Log startup message if we have any handlers
if logger._core.handlers:
logger.info("YTSage logging system initialized")
if log_dir and log_dir.exists():
logger.debug(f"Log directory: {log_dir}")
else:
logger.warning("File logging disabled - could not create log directory")
# If no handlers were successfully added, add a null handler to prevent errors
if not logger._core.handlers:
# Add a minimal handler that just discards messages
# This prevents loguru from complaining about no handlers
import tempfile
try:
# Try to add a temporary file handler as last resort
temp_log = Path(tempfile.gettempdir()) / "ytsage_temp.log"
logger.add(temp_log, level="ERROR", catch=True)
except Exception:
# If even that fails, we're in a very restricted environment
# loguru should handle this gracefully with its internal fallbacks
pass
return logger
def get_logger(name: str | None = None):
"""
Get a logger instance for a specific module.
Args:
name: Name of the module/component requesting the logger
Returns:
Configured logger instance
"""
if name:
return logger.bind(name=name)
return logger
# Initialize logging when module is imported - with maximum safety
_setup_complete = False
def safe_setup():
"""Safely initialize logging with multiple fallback strategies."""
global _setup_complete
if _setup_complete:
return logger
try:
setup_logging()
_setup_complete = True
except Exception:
# If all else fails, create an even simpler logger that just prints
if LOGURU_AVAILABLE:
try:
logger.remove()
except Exception:
pass
# At this point, just ensure we have something that won't crash
_setup_complete = True
return logger
# Try to set up logging, but don't let it crash the module import
try:
safe_setup()
except Exception:
# Ultimate fallback - the module will still import successfully
pass
# Export the main logger for convenience
__all__ = ["logger", "get_logger", "setup_logging"]
+108 -80
View File
@@ -4,28 +4,13 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
from importlib.metadata import PackageNotFoundError
from pathlib import Path from pathlib import Path
try:
from importlib.metadata import version as importlib_version
from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError
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
import requests import requests
from packaging import version from packaging import version
from src.core.ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path from src.core.ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
from src.core.ytsage_logging import logger
from src.core.ytsage_yt_dlp import get_yt_dlp_path from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import ( from src.utils.ytsage_constants import (
APP_CONFIG_FILE, APP_CONFIG_FILE,
@@ -35,6 +20,25 @@ from src.utils.ytsage_constants import (
YTDLP_APP_BIN_PATH, YTDLP_APP_BIN_PATH,
YTDLP_DOWNLOAD_URL, YTDLP_DOWNLOAD_URL,
) )
from src.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 # Cache for version information to avoid delays
_version_cache = { _version_cache = {
@@ -108,7 +112,7 @@ def load_version_cache_from_config() -> None:
if tool_name in _version_cache: if tool_name in _version_cache:
_version_cache[tool_name].update(cache_data) _version_cache[tool_name].update(cache_data)
except Exception as e: except Exception as e:
logger.error(f"Error loading version cache: {e}") logger.exception(f"Error loading version cache: {e}")
def save_version_cache_to_config() -> None: def save_version_cache_to_config() -> None:
@@ -118,7 +122,7 @@ def save_version_cache_to_config() -> None:
config["cached_versions"] = _version_cache.copy() config["cached_versions"] = _version_cache.copy()
save_config(config) save_config(config)
except Exception as e: except Exception as e:
logger.error(f"Error saving version cache: {e}") logger.exception(f"Error saving version cache: {e}")
def get_ytdlp_version_cached() -> str: def get_ytdlp_version_cached() -> str:
@@ -140,7 +144,7 @@ def get_ytdlp_version_cached() -> str:
return version_info return version_info
except Exception as e: except Exception as e:
logger.error(f"Error getting cached yt-dlp version: {e}") logger.exception(f"Error getting cached yt-dlp version: {e}")
return "Error getting version" return "Error getting version"
@@ -164,7 +168,7 @@ def get_ffmpeg_version_cached() -> str:
return version_info return version_info
except Exception as e: except Exception as e:
logger.error(f"Error getting cached FFmpeg version: {e}") logger.exception(f"Error getting cached FFmpeg version: {e}")
return "Error getting version" return "Error getting version"
@@ -182,7 +186,7 @@ def refresh_version_cache(force=False) -> bool:
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error refreshing version cache: {e}") logger.exception(f"Error refreshing version cache: {e}")
return False return False
@@ -215,7 +219,7 @@ def get_ytdlp_version_direct(yt_dlp_path=None) -> str:
else: else:
return "Error getting version" return "Error getting version"
except Exception as e: except Exception as e:
logger.error(f"Error getting yt-dlp version: {e}") logger.exception(f"Error getting yt-dlp version: {e}")
return "Error getting version" return "Error getting version"
@@ -269,10 +273,10 @@ def get_ffmpeg_version_direct() -> str:
return "Unknown version" return "Unknown version"
return "Not found" return "Not found"
except Exception as e: except Exception as e:
logger.error(f"Error getting FFmpeg version from install path: {e}") logger.exception(f"Error getting FFmpeg version from install path: {e}")
return "Not found" return "Not found"
except Exception as e: except Exception as e:
logger.error(f"Error getting FFmpeg version: {e}") logger.exception(f"Error getting FFmpeg version: {e}")
return "Error getting version" return "Error getting version"
@@ -308,7 +312,7 @@ def load_config() -> dict:
config[key] = value config[key] = value
return config return config
except (json.JSONDecodeError, UnicodeError, Exception) as e: except (json.JSONDecodeError, UnicodeError, Exception) as e:
logger.error(f"Error reading config file: {e}") logger.exception(f"Error reading config file: {e}")
# If config file is corrupted, create a new one with defaults # If config file is corrupted, create a new one with defaults
save_config(default_config) save_config(default_config)
@@ -322,7 +326,7 @@ def save_config(config) -> bool:
json.dump(config, f, ensure_ascii=False, indent=2) json.dump(config, f, ensure_ascii=False, indent=2)
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error saving config: {e}") logger.exception(f"Error saving config: {e}")
return False return False
@@ -342,7 +346,7 @@ def check_ffmpeg() -> bool:
os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}" os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error updating PATH: {e}") logger.exception(f"Error updating PATH: {e}")
return False return False
# For macOS, check common paths # For macOS, check common paths
@@ -359,13 +363,13 @@ def check_ffmpeg() -> bool:
os.environ["PATH"] = f"{ffmpeg_dir}{os.pathsep}{os.environ.get('PATH', '')}" os.environ["PATH"] = f"{ffmpeg_dir}{os.pathsep}{os.environ.get('PATH', '')}"
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error updating PATH: {e}") logger.exception(f"Error updating PATH: {e}")
continue continue
return False return False
except Exception as e: except Exception as e:
logger.error(f"Error checking FFmpeg: {e}") logger.exception(f"Error checking FFmpeg: {e}")
return False return False
@@ -381,7 +385,7 @@ def load_saved_path(main_window_instance) -> None:
main_window_instance.last_path = saved_path main_window_instance.last_path = saved_path
return return
except (json.JSONDecodeError, UnicodeError) as e: except (json.JSONDecodeError, UnicodeError) as e:
logger.error(f"Error reading config file: {e}") logger.exception(f"Error reading config file: {e}")
# If config file is corrupted, try to remove it # If config file is corrupted, try to remove it
try: try:
APP_CONFIG_FILE.unlink(missing_ok=True) APP_CONFIG_FILE.unlink(missing_ok=True)
@@ -397,7 +401,7 @@ def load_saved_path(main_window_instance) -> None:
main_window_instance.last_path = tempfile.gettempdir() main_window_instance.last_path = tempfile.gettempdir()
except Exception as e: except Exception as e:
logger.error(f"Error loading saved settings: {e}") logger.exception(f"Error loading saved settings: {e}")
main_window_instance.last_path = tempfile.gettempdir() main_window_instance.last_path = tempfile.gettempdir()
@@ -409,7 +413,7 @@ def save_path(main_window_instance, path) -> bool:
try: try:
Path(path).mkdir(exist_ok=True) Path(path).mkdir(exist_ok=True)
except Exception as e: except Exception as e:
logger.error(f"Error creating directory: {e}") logger.exception(f"Error creating directory: {e}")
return False return False
if not os.access(path, os.W_OK): if not os.access(path, os.W_OK):
@@ -423,7 +427,7 @@ def save_path(main_window_instance, path) -> bool:
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error saving settings: {e}") logger.exception(f"Error saving settings: {e}")
return False return False
@@ -484,13 +488,13 @@ def update_yt_dlp() -> bool:
logger.info("yt-dlp binary successfully updated") logger.info("yt-dlp binary successfully updated")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error replacing yt-dlp binary: {e}") logger.exception(f"Error replacing yt-dlp binary: {e}")
return False return False
else: else:
logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}") logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"Error downloading yt-dlp update: {e}") logger.exception(f"Error downloading yt-dlp update: {e}")
return False return False
else: else:
# We're using a system-installed yt-dlp, use pip to update # We're using a system-installed yt-dlp, use pip to update
@@ -540,9 +544,9 @@ def update_yt_dlp() -> bool:
else: else:
logger.info(f"Failed to get latest version info: HTTP {response.status_code}") logger.info(f"Failed to get latest version info: HTTP {response.status_code}")
except Exception as e: except Exception as e:
logger.error(f"Error checking for yt-dlp updates: {e}") logger.exception(f"Error checking for yt-dlp updates: {e}")
except Exception as e: except Exception as e:
logger.info(f"Unexpected error during yt-dlp update: {e}") logger.exception(f"Unexpected error during yt-dlp update: {e}")
return False return False
@@ -573,7 +577,7 @@ def should_check_for_auto_update() -> bool:
return False return False
except Exception as e: except Exception as e:
logger.error(f"Error checking auto-update schedule: {e}") logger.exception(f"Error checking auto-update schedule: {e}")
return False return False
@@ -602,9 +606,7 @@ def check_and_update_ytdlp_auto() -> bool:
logger.info(f"Latest yt-dlp version: {latest_version}") logger.info(f"Latest yt-dlp version: {latest_version}")
# Compare versions # Compare versions
from packaging import version as version_parser if version.parse(latest_version) > version.parse(current_version):
if version_parser.parse(latest_version) > version_parser.parse(current_version):
logger.info(f"Auto-updating yt-dlp from {current_version} to {latest_version}...") logger.info(f"Auto-updating yt-dlp from {current_version} to {latest_version}...")
# Perform the update # Perform the update
@@ -630,11 +632,11 @@ def check_and_update_ytdlp_auto() -> bool:
logger.info(f"Network error during auto-update check: {e}") logger.info(f"Network error during auto-update check: {e}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"Error during auto-update check: {e}") logger.exception(f"Error during auto-update check: {e}")
return False return False
except Exception as e: except Exception as e:
logger.info(f"Critical error in auto-update: {e}") logger.critical(f"Critical error in auto-update: {e}", exc_info=True)
return False return False
@@ -657,7 +659,7 @@ def update_auto_update_settings(enabled, frequency) -> bool:
save_config(config) save_config(config)
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error updating auto-update settings: {e}") logger.exception(f"Error updating auto-update settings: {e}")
return False return False
@@ -671,63 +673,89 @@ def parse_yt_dlp_error(error_message: str) -> str:
Returns: Returns:
str: A user-friendly error message with actionable advice str: A user-friendly error message with actionable advice
""" """
error_str = str(error_message).lower() error_str = error_message.lower()
# Private video errors # Private video errors
if any(keyword in error_str for keyword in ['private video', 'login_required', 'sign in if you']): if any(keyword in error_str for keyword in ["private video", "login_required", "sign in if you"]):
return ("This is a private video. You can download it by logging into your account using cookies.\n" return (
"Go to 'Custom Options''Login with Cookies''Extract cookies from browser' to authenticate.") "This is a private video. You can download it by logging into your account using cookies.\n"
"Go to 'Custom Options''Login with Cookies''Extract cookies from browser' to authenticate."
)
# Age-restricted content # Age-restricted content
if any(keyword in error_str for keyword in ['age restricted', 'age-restricted', 'confirm your age']): if any(keyword in error_str for keyword in ["age restricted", "age-restricted", "confirm your age"]):
return ("This video is age-restricted. You need to be logged in to access it.\n" return (
"Use 'Custom Options''Login with Cookies' to authenticate with your account.") "This video is age-restricted. You need to be logged in to access it.\n"
"Use 'Custom Options''Login with Cookies' to authenticate with your account."
)
# Geo-blocked content # 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']): if any(
return ("This video is not available in your region (geo-blocked).\n" keyword in error_str
"You may need to use a VPN or the video might be restricted in your country.") for keyword in [
"not available in your country",
"geo-blocked",
"video is not available",
"not made this video available in your country",
]
):
return (
"This video is not available in your region (geo-blocked).\n"
"You may need to use a VPN or the video might be restricted in your country."
)
# Removed/deleted videos # Removed/deleted videos
if any(keyword in error_str for keyword in ['video unavailable', 'this video has been removed', 'video does not exist']): if any(keyword in error_str for keyword in ["video unavailable", "this video has been removed", "video does not exist"]):
return ("This video has been removed or is no longer available.\n" return (
"The video may have been deleted by the uploader or removed due to policy violations.") "This video has been removed or is no longer available.\n"
"The video may have been deleted by the uploader or removed due to policy violations."
)
# Live stream errors # Live stream errors
if any(keyword in error_str for keyword in ['live stream', 'livestream', 'is live']): if any(keyword in error_str for keyword in ["live stream", "livestream", "is live"]):
return ("This is a live stream that cannot be downloaded while active.\n" return (
"Wait for the stream to end, then try downloading the archived version.") "This is a live stream that cannot be downloaded while active.\n"
"Wait for the stream to end, then try downloading the archived version."
)
# Playlist errors # Playlist errors
if any(keyword in error_str for keyword in ['playlist', 'no entries']): if any(keyword in error_str for keyword in ["playlist", "no entries"]):
return ("Unable to access this playlist. It may be private, deleted, or empty.\n" return (
"Check if the playlist exists and is publicly accessible.") "Unable to access this playlist. It may be private, deleted, or empty.\n"
"Check if the playlist exists and is publicly accessible."
)
# Network/connection errors # Network/connection errors
if any(keyword in error_str for keyword in ['network error', 'connection', 'timeout', 'unable to download']): if any(keyword in error_str for keyword in ["network error", "connection", "timeout", "unable to download"]):
return ("Network connection error. Please check your internet connection and try again.\n" return (
"If the problem persists, the video server might be temporarily unavailable.") "Network connection error. Please check your internet connection and try again.\n"
"If the problem persists, the video server might be temporarily unavailable."
)
# Invalid URL # Invalid URL
if any(keyword in error_str for keyword in ['invalid url', 'unsupported url', 'no video found']): if any(keyword in error_str for keyword in ["invalid url", "unsupported url", "no video found"]):
return ("Invalid or unsupported URL. Please check the link and try again.\n" return (
"Make sure you're using a valid YouTube, Vimeo, or other supported platform URL.") "Invalid or unsupported URL. Please check the link and try again.\n"
"Make sure you're using a valid YouTube, Vimeo, or other supported platform URL."
)
# YouTube premium content # YouTube premium content
if any(keyword in error_str for keyword in ['youtube premium', 'premium', 'members only']): if any(keyword in error_str for keyword in ["youtube premium", "premium", "members only"]):
return ("This content requires YouTube Premium or channel membership.\n" return (
"You need to be logged in with an account that has access to this content.") "This content requires YouTube Premium or channel membership.\n"
"You need to be logged in with an account that has access to this content."
)
# Copyright/DMCA # Copyright/DMCA
if any(keyword in error_str for keyword in ['copyright', 'dmca', 'blocked']): if any(keyword in error_str for keyword in ["copyright", "dmca", "blocked"]):
return ("This video is blocked due to copyright claims.\n" return "This video is blocked due to copyright claims.\n" "The content owner has restricted access to this video."
"The content owner has restricted access to this video.")
# Extraction errors (could be temporary) # Extraction errors (could be temporary)
if any(keyword in error_str for keyword in ['unable to extract', 'extraction failed']): if any(keyword in error_str for keyword in ["unable to extract", "extraction failed"]):
return ("Failed to extract video information. This might be a temporary issue.\n" return (
"Please try again in a few minutes, or check if the video link is correct.") "Failed to extract video information. This might be a temporary issue.\n"
"Please try again in a few minutes, or check if the video link is correct."
)
# Generic fallback with the original error for debugging # Generic fallback with the original error for debugging
return (f"Could not extract video information. Please check your link.\n" return f"Could not extract video information. Please check your link.\n" f"Technical details: {error_message}"
f"Technical details: {error_message}")
+8 -8
View File
@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.core.ytsage_logging import logger from src.utils.ytsage_logger import logger
from src.utils.ytsage_constants import ( from src.utils.ytsage_constants import (
APP_BIN_DIR, APP_BIN_DIR,
ICON_PATH, ICON_PATH,
@@ -94,7 +94,7 @@ class YtdlpSetupDialog(QDialog):
# icon_path logic moved to src\utils\ytsage_constants.py # icon_path logic moved to src\utils\ytsage_constants.py
icon_path = ICON_PATH icon_path = ICON_PATH
if Path.exists(icon_path): if Path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path.as_posix())) self.setWindowIcon(QIcon(str(icon_path)))
self.init_ui() self.init_ui()
@@ -390,11 +390,11 @@ class YtdlpSetupDialog(QDialog):
self.setup_complete.emit(target_path) self.setup_complete.emit(target_path)
self.accept() self.accept()
except Exception as copy_error: except Exception as copy_error:
logger.debug(f"Error copying file: {str(copy_error)}") logger.debug(f"Error copying file: {copy_error}", exc_info=True)
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical) error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Setup Error") error_dialog.setWindowTitle("Setup Error")
error_dialog.setText(f"Error copying yt-dlp to app directory: {str(copy_error)}") error_dialog.setText(f"Error copying yt-dlp to app directory: {copy_error}")
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
""" """
QMessageBox { QMessageBox {
@@ -448,11 +448,11 @@ class YtdlpSetupDialog(QDialog):
) )
error_dialog.exec() error_dialog.exec()
except Exception as e: except Exception as e:
logger.debug(f"Exception during verification: {str(e)}") logger.debug(f"Exception during verification: {e}", exc_info=True)
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Icon.Critical) error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Error") error_dialog.setWindowTitle("Error")
error_dialog.setText(f"Error verifying yt-dlp executable: {str(e)}") error_dialog.setText(f"Error verifying yt-dlp executable: {e}")
error_dialog.setStyleSheet( error_dialog.setStyleSheet(
""" """
QMessageBox { QMessageBox {
@@ -492,7 +492,7 @@ def check_ytdlp_binary() -> Optional[Path]:
os.chmod(exe_path, 0o755) os.chmod(exe_path, 0o755)
logger.info(f"Fixed permissions on yt-dlp at {exe_path}") logger.info(f"Fixed permissions on yt-dlp at {exe_path}")
except Exception as e: except Exception as e:
logger.warning(f"Could not set executable permissions on {exe_path}: {e}") logger.exception(f"Could not set executable permissions on {exe_path}: {e}")
return exe_path return exe_path
# If not found in app directory, check if yt-dlp is available in PATH # If not found in app directory, check if yt-dlp is available in PATH
@@ -517,7 +517,7 @@ def check_ytdlp_binary() -> Optional[Path]:
logger.info(f"Found yt-dlp in PATH: {yt_dlp_path}") logger.info(f"Found yt-dlp in PATH: {yt_dlp_path}")
return Path(yt_dlp_path) return Path(yt_dlp_path)
except Exception as e: except Exception as e:
logger.error(f"Error checking for yt-dlp in PATH: {e}") logger.exception(f"Error checking for yt-dlp in PATH: {e}")
# We're only interested in our app-specific installation or system PATH # We're only interested in our app-specific installation or system PATH
return None return None
+1 -4
View File
@@ -13,10 +13,7 @@ This package contains all dialog classes organized by functionality:
# Re-export all dialog classes for backward compatibility # Re-export all dialog classes for backward compatibility
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import ( from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog
CustomOptionsDialog,
TimeRangeDialog,
)
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import ( from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import (
PlaylistSelectionDialog, PlaylistSelectionDialog,
@@ -3,6 +3,8 @@ Base dialogs for YTSage application.
Contains basic utility dialogs like LogWindow and AboutDialog. Contains basic utility dialogs like LogWindow and AboutDialog.
""" """
from datetime import datetime
from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtCore import Qt, QThread, QTimer, Signal
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QDialog,
@@ -17,6 +19,8 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.utils.ytsage_localization import _
from src.core.ytsage_ffmpeg import get_ffmpeg_path from src.core.ytsage_ffmpeg import get_ffmpeg_path
from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, refresh_version_cache from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, refresh_version_cache
from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path
@@ -58,7 +62,7 @@ class AboutDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._parent = parent # Store parent to access version etc. self._parent = parent # Store parent to access version etc.
self.setWindowTitle("About YTSage") self.setWindowTitle(_("about.title"))
self.setMinimumSize(460, 420) # Slightly increased to accommodate paths self.setMinimumSize(460, 420) # Slightly increased to accommodate paths
self.resize(460, 440) # Slightly increased initial size self.resize(460, 440) # Slightly increased initial size
self.setMaximumSize(500, 480) # Reasonable maximum size self.setMaximumSize(500, 480) # Reasonable maximum size
@@ -155,13 +159,13 @@ class AboutDialog(QDialog):
layout.addWidget(title_label) layout.addWidget(title_label)
version_label = QLabel( version_label = QLabel(
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>Version {getattr(self._parent, 'version', '4.8.3')}</span>" f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>{_('about.version', version=getattr(self._parent, 'version', '4.9.0b'))}</span>"
) )
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter) version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(version_label) layout.addWidget(version_label)
# Description - more compact # Description - more compact
description_label = QLabel("Modern YouTube downloader with a clean PySide6 interface.") description_label = QLabel(_("about.description"))
description_label.setWordWrap(True) description_label.setWordWrap(True)
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter) description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;") description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
@@ -172,13 +176,13 @@ class AboutDialog(QDialog):
info_layout.setSpacing(15) info_layout.setSpacing(15)
author_label = QLabel( author_label = QLabel(
"By: <a href='https://github.com/oop7/' style='color: #c90000; text-decoration: none; font-size: 10px;'>oop7</a>" f"{_('about.author', author='<a href=\'https://github.com/oop7/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>oop7</a>')}"
) )
author_label.setOpenExternalLinks(True) author_label.setOpenExternalLinks(True)
info_layout.addWidget(author_label) info_layout.addWidget(author_label)
repo_label = QLabel( repo_label = QLabel(
"GitHub: <a href='https://github.com/oop7/YTSage/' style='color: #c90000; text-decoration: none; font-size: 10px;'>YTSage</a>" f"{_('about.github', repo='<a href=\'https://github.com/oop7/YTSage/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>YTSage</a>')}"
) )
repo_label.setOpenExternalLinks(True) repo_label.setOpenExternalLinks(True)
info_layout.addWidget(repo_label) info_layout.addWidget(repo_label)
@@ -217,7 +221,7 @@ class AboutDialog(QDialog):
header_layout.setContentsMargins(0, 0, 0, 5) header_layout.setContentsMargins(0, 0, 0, 5)
# System Information title # System Information title
title_label = QLabel("System Information") title_label = QLabel(_("about.system_info"))
title_label.setStyleSheet( title_label.setStyleSheet(
""" """
QLabel { QLabel {
@@ -235,7 +239,7 @@ class AboutDialog(QDialog):
header_layout.addStretch() header_layout.addStretch()
# Create refresh button # Create refresh button
self.refresh_btn = QPushButton("🔄") self.refresh_btn = QPushButton(_("about.refresh"))
self.refresh_btn.setFixedSize(16, 16) self.refresh_btn.setFixedSize(16, 16)
self.refresh_btn.setStyleSheet( self.refresh_btn.setStyleSheet(
""" """
@@ -281,7 +285,7 @@ class AboutDialog(QDialog):
def _show_loading_message(self) -> None: def _show_loading_message(self) -> None:
"""Show a compact loading message while system information is being gathered.""" """Show a compact loading message while system information is being gathered."""
loading_label = QLabel("🔄 Loading system information...") loading_label = QLabel(_("about.loading"))
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
loading_label.setStyleSheet( loading_label.setStyleSheet(
""" """
@@ -379,7 +383,7 @@ class AboutDialog(QDialog):
# yt-dlp Status - compact version with path # yt-dlp Status - compact version with path
ytdlp_found = check_ytdlp_installed() ytdlp_found = check_ytdlp_installed()
ytdlp_status_text = ( ytdlp_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>" if ytdlp_found else "<span style='color: #F44336;'>✗ Missing</span>" f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if ytdlp_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
) )
ytdlp_version = get_ytdlp_version() ytdlp_version = get_ytdlp_version()
@@ -392,8 +396,6 @@ class AboutDialog(QDialog):
last_check = ytdlp_cache.get("last_check", 0) last_check = ytdlp_cache.get("last_check", 0)
cache_status = "" cache_status = ""
if last_check > 0: if last_check > 0:
from datetime import datetime
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
@@ -409,11 +411,11 @@ class AboutDialog(QDialog):
# FFmpeg Status - compact version with path # FFmpeg Status - compact version with path
ffmpeg_found = check_ffmpeg() ffmpeg_found = check_ffmpeg()
ffmpeg_status_text = ( ffmpeg_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>" f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
if ffmpeg_found if ffmpeg_found
else "<span style='color: #F44336;'>✗ Missing</span>" else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
) )
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else "Not Available" ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else _('about.not_available')
# Get FFmpeg path # Get FFmpeg path
ffmpeg_path_text = None ffmpeg_path_text = None
@@ -426,8 +428,6 @@ class AboutDialog(QDialog):
last_check = ffmpeg_cache.get("last_check", 0) last_check = ffmpeg_cache.get("last_check", 0)
cache_status = "" cache_status = ""
if last_check > 0 and ffmpeg_found: if last_check > 0 and ffmpeg_found:
from datetime import datetime
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M") cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
@@ -442,7 +442,7 @@ class AboutDialog(QDialog):
def refresh_version_info(self) -> None: def refresh_version_info(self) -> None:
"""Refresh version information manually.""" """Refresh version information manually."""
self.refresh_btn.setText("🔄 Refreshing...") self.refresh_btn.setText(_('about.refreshing'))
self.refresh_btn.setEnabled(False) self.refresh_btn.setEnabled(False)
# Perform refresh in a separate thread to avoid blocking UI # Perform refresh in a separate thread to avoid blocking UI
@@ -459,7 +459,7 @@ class AboutDialog(QDialog):
def on_refresh_finished(self, success) -> None: def on_refresh_finished(self, success) -> None:
"""Handle refresh completion.""" """Handle refresh completion."""
self.refresh_btn.setText("🔄 Refresh") self.refresh_btn.setText(_('about.refresh'))
self.refresh_btn.setEnabled(True) self.refresh_btn.setEnabled(True)
if success: if success:
@@ -468,8 +468,8 @@ class AboutDialog(QDialog):
# Show error message with proper styling # Show error message with proper styling
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Icon.Warning) msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("Refresh Failed") msg_box.setWindowTitle(_('about.refresh_failed'))
msg_box.setText("Failed to refresh version information.") msg_box.setText(_('about.refresh_failed_message'))
msg_box.setWindowIcon(self.windowIcon()) msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet( msg_box.setStyleSheet(
""" """
@@ -8,7 +8,7 @@ import threading
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
from PySide6.QtCore import Q_ARG, QMetaObject, Qt, Signal, QObject from PySide6.QtCore import Q_ARG, QMetaObject, QObject, Qt, Signal
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QCheckBox, QCheckBox,
QComboBox, QComboBox,
@@ -30,13 +30,9 @@ from PySide6.QtWidgets import (
from src.core.ytsage_yt_dlp import get_yt_dlp_path from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import YTDLP_DOCS_URL from src.utils.ytsage_constants import YTDLP_DOCS_URL
from src.utils.ytsage_config_manager import ConfigManager
try: from src.utils.ytsage_localization import LocalizationManager, _
import yt_dlp from src.utils.ytsage_logger import logger
YT_DLP_AVAILABLE = True
except ImportError:
YT_DLP_AVAILABLE = False
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import) from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
@@ -74,7 +70,7 @@ class CommandWorker(QObject):
base_cmd.append(self.url) base_cmd.append(self.url)
# Emit the full command # Emit the full command
self.output_received.emit(f"🔧 Full command: {' '.join(str(cmd) for cmd in base_cmd)}") self.output_received.emit(_('custom_command.full_command', command=' '.join(str(cmd) for cmd in base_cmd)))
self.output_received.emit("=" * 50) self.output_received.emit("=" * 50)
# Run the command # Run the command
@@ -96,22 +92,22 @@ class CommandWorker(QObject):
self.output_received.emit("=" * 50) self.output_received.emit("=" * 50)
if ret != 0: if ret != 0:
self.output_received.emit(f"❌ Command failed with exit code {ret}") self.output_received.emit(_('custom_command.command_failed', code=ret))
self.command_finished.emit(False, ret) self.command_finished.emit(False, ret)
else: else:
self.output_received.emit("✅ Command completed successfully!") self.output_received.emit(_('custom_command.command_success'))
self.command_finished.emit(True, ret) self.command_finished.emit(True, ret)
except Exception as e: except Exception as e:
self.output_received.emit("=" * 50) self.output_received.emit("=" * 50)
self.error_occurred.emit(f"❌ Error executing command: {str(e)}") self.error_occurred.emit(_('custom_command.command_error', error=str(e)))
class CustomOptionsDialog(QDialog): class CustomOptionsDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking. self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking.
self.setWindowTitle("Custom Options") self.setWindowTitle(_("dialogs.custom_options"))
self.setMinimumSize(550, 400) # Made even shorter self.setMinimumSize(550, 400) # Made even shorter
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@@ -124,47 +120,44 @@ class CustomOptionsDialog(QDialog):
cookies_layout = QVBoxLayout(cookies_tab) cookies_layout = QVBoxLayout(cookies_tab)
# Help text # Help text
help_text = QLabel( help_text = QLabel(_('cookies.help_text'))
"Choose how to provide cookies for logging in.\n"
"This allows downloading of private videos and premium quality audio."
)
help_text.setWordWrap(True) help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;") help_text.setStyleSheet("color: #999999; padding: 10px;")
cookies_layout.addWidget(help_text) cookies_layout.addWidget(help_text)
# Cookie source selection # Cookie source selection
cookie_source_group = QGroupBox("Cookie Source") cookie_source_group = QGroupBox(_('cookies.cookie_source'))
cookie_source_layout = QVBoxLayout(cookie_source_group) cookie_source_layout = QVBoxLayout(cookie_source_group)
# Radio buttons for cookie source # Radio buttons for cookie source
self.cookie_file_radio = QRadioButton("Use cookie file (Netscape format)") self.cookie_file_radio = QRadioButton(_('cookies.use_cookie_file'))
self.cookie_file_radio.setChecked(True) self.cookie_file_radio.setChecked(True)
self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed) self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_file_radio) cookie_source_layout.addWidget(self.cookie_file_radio)
self.cookie_browser_radio = QRadioButton("Extract cookies from browser") self.cookie_browser_radio = QRadioButton(_('cookies.extract_from_browser'))
self.cookie_browser_radio.toggled.connect(self.on_cookie_source_changed) self.cookie_browser_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_browser_radio) cookie_source_layout.addWidget(self.cookie_browser_radio)
cookies_layout.addWidget(cookie_source_group) cookies_layout.addWidget(cookie_source_group)
# Cookie file section # Cookie file section
self.cookie_file_group = QGroupBox("Cookie File") self.cookie_file_group = QGroupBox(_('cookies.cookie_file'))
file_layout = QVBoxLayout(self.cookie_file_group) file_layout = QVBoxLayout(self.cookie_file_group)
# File path input and browse button # File path input and browse button
path_layout = QHBoxLayout() path_layout = QHBoxLayout()
self.cookie_path_input = QLineEdit() self.cookie_path_input = QLineEdit()
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)") self.cookie_path_input.setPlaceholderText(_('cookies.cookie_file_placeholder'))
if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path: if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
# Convert Path to string properly and validate # Convert Path to string properly and validate
cookie_path_str = str(self._parent.cookie_file_path) cookie_path_str = str(self._parent.cookie_file_path)
# Only set if it looks like a valid path (more than just a drive letter) # Only set if it looks like a valid path (more than just a drive letter)
if len(cookie_path_str) > 3 and not cookie_path_str.endswith(':'): if len(cookie_path_str) > 3 and not cookie_path_str.endswith(":"):
self.cookie_path_input.setText(cookie_path_str) self.cookie_path_input.setText(cookie_path_str)
path_layout.addWidget(self.cookie_path_input) path_layout.addWidget(self.cookie_path_input)
self.browse_button = QPushButton("Browse") self.browse_button = QPushButton(_('buttons.browse'))
self.browse_button.clicked.connect(self.browse_cookie_file) self.browse_button.clicked.connect(self.browse_cookie_file)
path_layout.addWidget(self.browse_button) path_layout.addWidget(self.browse_button)
file_layout.addLayout(path_layout) file_layout.addLayout(path_layout)
@@ -172,38 +165,27 @@ class CustomOptionsDialog(QDialog):
cookies_layout.addWidget(self.cookie_file_group) cookies_layout.addWidget(self.cookie_file_group)
# Browser selection section # Browser selection section
self.cookie_browser_group = QGroupBox("Browser Selection") self.cookie_browser_group = QGroupBox(_('cookies.browser_selection'))
browser_layout = QVBoxLayout(self.cookie_browser_group) browser_layout = QVBoxLayout(self.cookie_browser_group)
browser_help = QLabel( browser_help = QLabel(_('cookies.browser_help'))
"Select the browser to extract cookies from. Make sure the browser is closed before extraction."
)
browser_help.setWordWrap(True) browser_help.setWordWrap(True)
browser_help.setStyleSheet("color: #999999; font-size: 11px;") browser_help.setStyleSheet("color: #999999; font-size: 11px;")
browser_layout.addWidget(browser_help) browser_layout.addWidget(browser_help)
browser_select_layout = QHBoxLayout() browser_select_layout = QHBoxLayout()
browser_select_layout.addWidget(QLabel("Browser:")) browser_select_layout.addWidget(QLabel(_('cookies.browser_label')))
self.browser_combo = QComboBox() self.browser_combo = QComboBox()
self.browser_combo.addItems([ self.browser_combo.addItems(["chrome", "firefox", "safari", "edge", "opera", "brave", "chromium", "vivaldi"])
"chrome",
"firefox",
"safari",
"edge",
"opera",
"brave",
"chromium",
"vivaldi"
])
browser_select_layout.addWidget(self.browser_combo) browser_select_layout.addWidget(self.browser_combo)
browser_layout.addLayout(browser_select_layout) browser_layout.addLayout(browser_select_layout)
# Optional profile field # Optional profile field
profile_layout = QHBoxLayout() profile_layout = QHBoxLayout()
profile_layout.addWidget(QLabel("Profile (optional):")) profile_layout.addWidget(QLabel(_('cookies.profile_label')))
self.profile_input = QLineEdit() self.profile_input = QLineEdit()
self.profile_input.setPlaceholderText("Profile name or path (leave empty for default)") self.profile_input.setPlaceholderText(_('cookies.profile_placeholder'))
profile_layout.addWidget(self.profile_input) profile_layout.addWidget(self.profile_input)
browser_layout.addLayout(profile_layout) browser_layout.addLayout(profile_layout)
@@ -224,12 +206,7 @@ class CustomOptionsDialog(QDialog):
command_layout = QVBoxLayout(command_tab) command_layout = QVBoxLayout(command_tab)
# Improved help text # Improved help text
cmd_help_text = QLabel( cmd_help_text = QLabel(_('custom_command.help_text', docs_url=YTDLP_DOCS_URL))
"Enter your custom yt-dlp command below. The current URL will be automatically appended.<br><br>"
"For complete list of options and usage examples, "
f'<a href="{YTDLP_DOCS_URL}">click here to view the official yt-dlp documentation</a>.<br><br>'
"Note: Download path and output filename template will be automatically handled."
)
cmd_help_text.setWordWrap(True) cmd_help_text.setWordWrap(True)
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
@@ -255,16 +232,13 @@ class CustomOptionsDialog(QDialog):
command_layout.addWidget(cmd_help_text) command_layout.addWidget(cmd_help_text)
# Command input label # Command input label
input_label = QLabel("yt-dlp Arguments:") input_label = QLabel(_('custom_command.input_label'))
input_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 10px;") input_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 10px;")
command_layout.addWidget(input_label) command_layout.addWidget(input_label)
# Command input # Command input
self.command_input = QPlainTextEdit() self.command_input = QPlainTextEdit()
self.command_input.setPlaceholderText( self.command_input.setPlaceholderText(_('custom_command.input_placeholder'))
"Enter yt-dlp arguments here...\n\n"
"e.g. --extract-audio --audio-format mp3"
)
self.command_input.setMinimumHeight(80) # Reduced further from 100 self.command_input.setMinimumHeight(80) # Reduced further from 100
self.command_input.setStyleSheet( self.command_input.setStyleSheet(
""" """
@@ -289,7 +263,7 @@ class CustomOptionsDialog(QDialog):
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
button_layout.setSpacing(10) button_layout.setSpacing(10)
clear_btn = QPushButton("Clear") clear_btn = QPushButton(_('buttons.clear'))
clear_btn.clicked.connect(lambda: self.command_input.clear()) clear_btn.clicked.connect(lambda: self.command_input.clear())
clear_btn.setStyleSheet( clear_btn.setStyleSheet(
""" """
@@ -312,7 +286,7 @@ class CustomOptionsDialog(QDialog):
button_layout.addStretch() # Push run button to the right button_layout.addStretch() # Push run button to the right
# Run command button # Run command button
self.run_btn = QPushButton("Run Command") self.run_btn = QPushButton(_('buttons.run_command'))
self.run_btn.clicked.connect(self.run_custom_command) self.run_btn.clicked.connect(self.run_custom_command)
self.run_btn.setDefault(True) self.run_btn.setDefault(True)
button_layout.addWidget(self.run_btn) button_layout.addWidget(self.run_btn)
@@ -320,14 +294,14 @@ class CustomOptionsDialog(QDialog):
command_layout.addLayout(button_layout) command_layout.addLayout(button_layout)
# Output label # Output label
output_label = QLabel("Command Output:") output_label = QLabel(_('custom_command.output_label'))
output_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 15px;") output_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 15px;")
command_layout.addWidget(output_label) command_layout.addWidget(output_label)
# Log output # Log output
self.log_output = QTextEdit() self.log_output = QTextEdit()
self.log_output.setReadOnly(True) self.log_output.setReadOnly(True)
self.log_output.setPlaceholderText("Command output will appear here...") self.log_output.setPlaceholderText(_('custom_command.output_placeholder'))
self.log_output.setMinimumHeight(100) # Reduced further from 120 self.log_output.setMinimumHeight(100) # Reduced further from 120
self.log_output.setStyleSheet( self.log_output.setStyleSheet(
""" """
@@ -347,12 +321,180 @@ class CustomOptionsDialog(QDialog):
) )
command_layout.addWidget(self.log_output) command_layout.addWidget(self.log_output)
# === Proxy Tab ===
proxy_tab = QWidget()
proxy_layout = QVBoxLayout(proxy_tab)
# Help text
proxy_help_text = QLabel(_('proxy.help_text'))
proxy_help_text.setWordWrap(True)
proxy_help_text.setStyleSheet("color: #999999; padding: 10px;")
proxy_layout.addWidget(proxy_help_text)
# Main Proxy section
main_proxy_group = QGroupBox(_('proxy.main_proxy'))
main_proxy_layout = QVBoxLayout(main_proxy_group)
main_proxy_help = QLabel(_('proxy.main_proxy_help'))
main_proxy_help.setWordWrap(True)
main_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
main_proxy_layout.addWidget(main_proxy_help)
# Main proxy input
main_proxy_input_layout = QHBoxLayout()
main_proxy_input_layout.addWidget(QLabel(_('proxy.proxy_url_label')))
self.proxy_url_input = QLineEdit()
self.proxy_url_input.setPlaceholderText(_('proxy.proxy_url_placeholder'))
self.proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
main_proxy_input_layout.addWidget(self.proxy_url_input)
main_proxy_layout.addLayout(main_proxy_input_layout)
# Example text
example_label = QLabel(_('proxy.proxy_examples'))
example_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;")
main_proxy_layout.addWidget(example_label)
proxy_layout.addWidget(main_proxy_group)
# Geo-verification Proxy section
geo_proxy_group = QGroupBox(_('proxy.geo_proxy'))
geo_proxy_layout = QVBoxLayout(geo_proxy_group)
geo_proxy_help = QLabel(_('proxy.geo_proxy_help'))
geo_proxy_help.setWordWrap(True)
geo_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
geo_proxy_layout.addWidget(geo_proxy_help)
# Geo proxy input
geo_proxy_input_layout = QHBoxLayout()
geo_proxy_input_layout.addWidget(QLabel(_('proxy.geo_proxy_url_label')))
self.geo_proxy_url_input = QLineEdit()
self.geo_proxy_url_input.setPlaceholderText(_('proxy.geo_proxy_url_placeholder'))
self.geo_proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
geo_proxy_input_layout.addWidget(self.geo_proxy_url_input)
geo_proxy_layout.addLayout(geo_proxy_input_layout)
proxy_layout.addWidget(geo_proxy_group)
# Proxy status indicator
self.proxy_status = QLabel("")
self.proxy_status.setStyleSheet("color: #999999; font-style: italic;")
proxy_layout.addWidget(self.proxy_status)
# Clear buttons
clear_layout = QHBoxLayout()
clear_main_proxy_btn = QPushButton(_('proxy.clear_main_proxy'))
clear_main_proxy_btn.clicked.connect(lambda: self.proxy_url_input.clear())
clear_main_proxy_btn.setStyleSheet(
"""
QPushButton {
padding: 6px 12px;
background-color: #444444;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #555555;
}
"""
)
clear_layout.addWidget(clear_main_proxy_btn)
clear_geo_proxy_btn = QPushButton(_('proxy.clear_geo_proxy'))
clear_geo_proxy_btn.clicked.connect(lambda: self.geo_proxy_url_input.clear())
clear_geo_proxy_btn.setStyleSheet(
"""
QPushButton {
padding: 6px 12px;
background-color: #444444;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #555555;
}
"""
)
clear_layout.addWidget(clear_geo_proxy_btn)
clear_layout.addStretch()
proxy_layout.addLayout(clear_layout)
proxy_layout.addStretch()
# === Language Tab ===
language_tab = QWidget()
language_layout = QVBoxLayout(language_tab)
# Help text
language_help_text = QLabel(_("language.select_language"))
language_help_text.setWordWrap(True)
language_help_text.setStyleSheet("color: #ffffff; font-size: 14px; font-weight: bold; padding: 10px;")
language_layout.addWidget(language_help_text)
# Current language info
current_lang = ConfigManager.get("language") or "en"
available_languages = LocalizationManager.get_available_languages()
current_lang_display = available_languages.get(current_lang, current_lang.upper())
current_lang_label = QLabel(_("language.current_language", language=current_lang_display))
current_lang_label.setWordWrap(True)
current_lang_label.setStyleSheet("color: #999999; padding: 10px;")
language_layout.addWidget(current_lang_label)
# Language selection group
language_group = QGroupBox(_("language.select_language"))
language_group_layout = QVBoxLayout(language_group)
# Language selection combo box
language_select_layout = QHBoxLayout()
language_select_layout.addWidget(QLabel(_("language.select_language") + ":"))
self.language_combo = QComboBox()
# Populate language combo with available languages
for lang_code, display_name in available_languages.items():
self.language_combo.addItem(display_name, lang_code)
# Set current selection
current_index = self.language_combo.findData(current_lang)
if current_index >= 0:
self.language_combo.setCurrentIndex(current_index)
# Connect language change event
self.language_combo.currentIndexChanged.connect(self.on_language_changed)
language_select_layout.addWidget(self.language_combo)
language_group_layout.addLayout(language_select_layout)
language_layout.addWidget(language_group)
# Restart notice
self.restart_notice = QLabel(_("language.restart_required"))
self.restart_notice.setWordWrap(True)
self.restart_notice.setStyleSheet(
"color: #ffaa00; font-style: italic; padding: 10px; "
"background-color: #2a2d36; border-radius: 6px; margin: 10px;"
)
self.restart_notice.setVisible(False) # Initially hidden
language_layout.addWidget(self.restart_notice)
language_layout.addStretch()
# Add tabs to the tab widget # Add tabs to the tab widget
self.tab_widget.addTab(cookies_tab, "Login with Cookies") self.tab_widget.addTab(cookies_tab, _("tabs.cookies"))
self.tab_widget.addTab(command_tab, "Custom Command") self.tab_widget.addTab(command_tab, _("tabs.custom_command"))
self.tab_widget.addTab(proxy_tab, _("tabs.proxy"))
self.tab_widget.addTab(language_tab, _("tabs.language"))
# Dialog buttons # Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
@@ -432,7 +574,6 @@ class CustomOptionsDialog(QDialog):
border: none; border: none;
width: 12px; width: 12px;
height: 12px; height: 12px;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
} }
QComboBox QAbstractItemView { QComboBox QAbstractItemView {
background-color: #1d1e22; background-color: #1d1e22;
@@ -463,13 +604,14 @@ class CustomOptionsDialog(QDialog):
# Initialize dialog with current settings (after all widgets and styles are set) # Initialize dialog with current settings (after all widgets and styles are set)
self._initialize_cookie_settings() self._initialize_cookie_settings()
self._initialize_proxy_settings()
def _initialize_cookie_settings(self) -> None: def _initialize_cookie_settings(self) -> None:
"""Initialize the dialog with current cookie settings from parent""" """Initialize the dialog with current cookie settings from parent"""
if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option: if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option:
# Browser cookies are active # Browser cookies are active
self.cookie_browser_radio.setChecked(True) self.cookie_browser_radio.setChecked(True)
browser_parts = self._parent.browser_cookies_option.split(':') browser_parts = self._parent.browser_cookies_option.split(":")
browser = browser_parts[0] browser = browser_parts[0]
profile = browser_parts[1] if len(browser_parts) > 1 else "" profile = browser_parts[1] if len(browser_parts) > 1 else ""
@@ -492,6 +634,22 @@ class CustomOptionsDialog(QDialog):
# No cookies configured - ensure file radio is selected by default # No cookies configured - ensure file radio is selected by default
self.cookie_file_radio.setChecked(True) self.cookie_file_radio.setChecked(True)
def _initialize_proxy_settings(self) -> None:
"""Initialize the dialog with current proxy settings from config"""
# Load proxy settings from config
proxy_url = ConfigManager.get("proxy_url")
geo_proxy_url = ConfigManager.get("geo_proxy_url")
# Set proxy field values if they exist
if proxy_url:
self.proxy_url_input.setText(proxy_url)
if geo_proxy_url:
self.geo_proxy_url_input.setText(geo_proxy_url)
# Update validation status
self.validate_proxy_inputs()
def on_cookie_source_changed(self) -> None: def on_cookie_source_changed(self) -> None:
"""Handle cookie source radio button changes""" """Handle cookie source radio button changes"""
if self.cookie_file_radio.isChecked(): if self.cookie_file_radio.isChecked():
@@ -501,18 +659,18 @@ class CustomOptionsDialog(QDialog):
else: else:
self.cookie_file_group.setVisible(False) self.cookie_file_group.setVisible(False)
self.cookie_browser_group.setVisible(True) self.cookie_browser_group.setVisible(True)
self.cookie_status.setText("Browser cookies will be extracted when applied") self.cookie_status.setText(_("cookies.browser_extract_message"))
self.cookie_status.setStyleSheet("color: #ffaa00; font-style: italic;") self.cookie_status.setStyleSheet("color: #ffaa00; font-style: italic;")
def browse_cookie_file(self) -> None: def browse_cookie_file(self) -> None:
# Open file dialog to select cookie file # Open file dialog to select cookie file
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)") selected_files, _ = QFileDialog.getOpenFileName(self, _("cookies.select_file_title"), "", _("cookies.file_filter"))
if selected_files: if selected_files:
# Ensure we have a valid full path # Ensure we have a valid full path
cookie_path = Path(selected_files).resolve() cookie_path = Path(selected_files).resolve()
self.cookie_path_input.setText(str(cookie_path)) self.cookie_path_input.setText(str(cookie_path))
self.cookie_status.setText("Cookie file selected - Click OK to apply") self.cookie_status.setText(_("cookies.file_selected_message"))
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;") self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
def get_cookie_file_path(self) -> Path | None: def get_cookie_file_path(self) -> Path | None:
@@ -544,6 +702,91 @@ class CustomOptionsDialog(QDialog):
"""Returns True if browser cookies mode is selected""" """Returns True if browser cookies mode is selected"""
return self.cookie_browser_radio.isChecked() return self.cookie_browser_radio.isChecked()
def get_proxy_url(self) -> str | None:
"""Returns the main proxy URL if specified"""
proxy_url = self.proxy_url_input.text().strip()
return proxy_url if proxy_url else None
def get_geo_proxy_url(self) -> str | None:
"""Returns the geo-verification proxy URL if specified"""
geo_proxy_url = self.geo_proxy_url_input.text().strip()
return geo_proxy_url if geo_proxy_url else None
def validate_proxy_url(self, url: str) -> bool:
"""Basic validation for proxy URL format"""
if not url:
return True # Empty is OK
# Check if it starts with a valid scheme
valid_schemes = ['http://', 'https://', 'socks5://', 'socks4://']
if not any(url.lower().startswith(scheme) for scheme in valid_schemes):
return False
# Basic URL format check (contains at least host:port)
try:
# Remove the scheme to check host:port part
for scheme in valid_schemes:
if url.lower().startswith(scheme):
host_port = url[len(scheme):]
break
# Skip user:pass@ part if present
if '@' in host_port:
host_port = host_port.split('@')[1]
# Should have at least host:port
if ':' in host_port:
host, port = host_port.split(':', 1)
if host and port.isdigit():
return True
return False
except:
return False
def validate_proxy_inputs(self) -> None:
"""Validate proxy inputs and update status"""
main_proxy = self.proxy_url_input.text().strip()
geo_proxy = self.geo_proxy_url_input.text().strip()
if not main_proxy and not geo_proxy:
# Check if there are saved settings
saved_main = ConfigManager.get("proxy_url")
saved_geo = ConfigManager.get("geo_proxy_url")
if saved_main or saved_geo:
status_parts = []
if saved_main:
status_parts.append(f"Saved main proxy: {saved_main}")
if saved_geo:
status_parts.append(f"Saved geo proxy: {saved_geo}")
self.proxy_status.setText(" | ".join(status_parts))
self.proxy_status.setStyleSheet("color: #888888; font-style: italic;")
else:
self.proxy_status.setText("")
return
issues = []
if main_proxy and not self.validate_proxy_url(main_proxy):
issues.append("Invalid main proxy URL format")
if geo_proxy and not self.validate_proxy_url(geo_proxy):
issues.append("Invalid geo proxy URL format")
if issues:
self.proxy_status.setText(" | ".join(issues))
self.proxy_status.setStyleSheet("color: #ff6666; font-style: italic;")
else:
status_parts = []
if main_proxy:
status_parts.append("Main proxy configured")
if geo_proxy:
status_parts.append("Geo proxy configured")
self.proxy_status.setText(" | ".join(status_parts))
self.proxy_status.setStyleSheet("color: #00cc00; font-style: italic;")
def run_custom_command(self) -> None: def run_custom_command(self) -> None:
url = self._parent.url_input.text().strip() url = self._parent.url_input.text().strip()
if not url: if not url:
@@ -566,7 +809,7 @@ class CustomOptionsDialog(QDialog):
self.log_output.append(f"📁 Download path: {path}") self.log_output.append(f"📁 Download path: {path}")
self.log_output.append("=" * 50) self.log_output.append("=" * 50)
self.run_btn.setEnabled(False) self.run_btn.setEnabled(False)
self.run_btn.setText("Running...") self.run_btn.setText(_("command.running"))
# Create worker and thread # Create worker and thread
self.worker = CommandWorker(command, url, path) self.worker = CommandWorker(command, url, path)
@@ -587,49 +830,63 @@ class CustomOptionsDialog(QDialog):
def on_command_finished(self, success: bool, exit_code: int): def on_command_finished(self, success: bool, exit_code: int):
"""Slot for when command finishes""" """Slot for when command finishes"""
self.run_btn.setEnabled(True) self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command") self.run_btn.setText(_("command.run_command"))
def on_error_occurred(self, error_msg: str): def on_error_occurred(self, error_msg: str):
"""Slot for handling errors""" """Slot for handling errors"""
self.log_output.append(error_msg) self.log_output.append(error_msg)
self.run_btn.setEnabled(True) self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command") self.run_btn.setText(_("buttons.run_command"))
def on_language_changed(self) -> None:
"""Handle language selection change"""
selected_lang_code = self.language_combo.currentData()
if selected_lang_code:
current_lang = ConfigManager.get("language") or "en"
if selected_lang_code != current_lang:
# Save the new language preference
ConfigManager.set("language", selected_lang_code)
# Update LocalizationManager
LocalizationManager.set_language(selected_lang_code)
# Show restart notice
self.restart_notice.setVisible(True)
logger.info(f"Language changed to: {selected_lang_code}")
class TimeRangeDialog(QDialog): class TimeRangeDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Download Video Section") self.setWindowTitle(_('time_range.title'))
self.setMinimumWidth(400) self.setMinimumWidth(400)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Help text explaining the feature # Help text explaining the feature
help_text = QLabel( help_text = QLabel(_('time_range.help_text'))
"Download only specific parts of a video by specifying time ranges.\n"
"Use HH:MM:SS format or seconds. Leave start or end empty to download from beginning or to end."
)
help_text.setWordWrap(True) help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;") help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text) layout.addWidget(help_text)
# Time range section # Time range section
time_group = QGroupBox("Time Range") time_group = QGroupBox(_('time_range.time_range_group'))
time_layout = QVBoxLayout() time_layout = QVBoxLayout()
# Start time row # Start time row
start_layout = QHBoxLayout() start_layout = QHBoxLayout()
start_layout.addWidget(QLabel("Start Time:")) start_layout.addWidget(QLabel(_('time_range.start_time')))
self.start_time_input = QLineEdit() self.start_time_input = QLineEdit()
self.start_time_input.setPlaceholderText("00:00:00 (or leave empty for start)") self.start_time_input.setPlaceholderText(_('time_range.start_time_placeholder'))
start_layout.addWidget(self.start_time_input) start_layout.addWidget(self.start_time_input)
time_layout.addLayout(start_layout) time_layout.addLayout(start_layout)
# End time row # End time row
end_layout = QHBoxLayout() end_layout = QHBoxLayout()
end_layout.addWidget(QLabel("End Time:")) end_layout.addWidget(QLabel(_('time_range.end_time')))
self.end_time_input = QLineEdit() self.end_time_input = QLineEdit()
self.end_time_input.setPlaceholderText("00:10:00 (or leave empty for end)") self.end_time_input.setPlaceholderText(_('time_range.end_time_placeholder'))
end_layout.addWidget(self.end_time_input) end_layout.addWidget(self.end_time_input)
time_layout.addLayout(end_layout) time_layout.addLayout(end_layout)
@@ -637,7 +894,7 @@ class TimeRangeDialog(QDialog):
layout.addWidget(time_group) layout.addWidget(time_group)
# Force keyframes option # Force keyframes option
self.force_keyframes = QCheckBox("Force keyframes at cuts (better accuracy, slower)") self.force_keyframes = QCheckBox(_('time_range.force_keyframes'))
self.force_keyframes.setChecked(True) self.force_keyframes.setChecked(True)
self.force_keyframes.setStyleSheet( self.force_keyframes.setStyleSheet(
""" """
@@ -665,7 +922,9 @@ class TimeRangeDialog(QDialog):
layout.addWidget(self.force_keyframes) layout.addWidget(self.force_keyframes)
# Buttons # Buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
@@ -48,7 +48,7 @@ class FFmpegCheckDialog(QDialog):
# icon_path logic moved to src\utils\ytsage_constants.py # icon_path logic moved to src\utils\ytsage_constants.py
if ICON_PATH.exists(): if ICON_PATH.exists():
self.setWindowIcon(QIcon(ICON_PATH.as_posix())) self.setWindowIcon(QIcon(str(ICON_PATH)))
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setSpacing(15) layout.setSpacing(15)
@@ -17,11 +17,13 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.utils.ytsage_localization import _
class SubtitleSelectionDialog(QDialog): class SubtitleSelectionDialog(QDialog):
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None: def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Select Subtitles") self.setWindowTitle(_("dialogs.select_subtitles"))
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setMinimumHeight(300) self.setMinimumHeight(300)
@@ -35,7 +37,7 @@ class SubtitleSelectionDialog(QDialog):
# Filter input # Filter input
self.filter_input = QLineEdit() self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText("Filter languages (e.g., en, es)...") self.filter_input.setPlaceholderText(_("dialogs.filter_languages_placeholder"))
self.filter_input.textChanged.connect(self.filter_list) self.filter_input.textChanged.connect(self.filter_list)
self.filter_input.setStyleSheet( self.filter_input.setStyleSheet(
""" """
@@ -72,7 +74,9 @@ class SubtitleSelectionDialog(QDialog):
self.populate_list() self.populate_list()
# OK and Cancel buttons # OK and Cancel buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@@ -128,7 +132,8 @@ class SubtitleSelectionDialog(QDialog):
combined_subs[lang_code] = f"{lang_code} - Auto-generated" combined_subs[lang_code] = f"{lang_code} - Auto-generated"
if not combined_subs: if not combined_subs:
no_subs_label = QLabel("No subtitles available" + (f" matching '{filter_text}'" if filter_text else "")) 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;") no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
self.list_layout.addWidget(no_subs_label) self.list_layout.addWidget(no_subs_label)
return return
@@ -193,7 +198,7 @@ class SubtitleSelectionDialog(QDialog):
class PlaylistSelectionDialog(QDialog): class PlaylistSelectionDialog(QDialog):
def __init__(self, playlist_entries, previously_selected_string, parent=None) -> None: def __init__(self, playlist_entries, previously_selected_string, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Select Playlist Videos") self.setWindowTitle(_("playlist.select_videos_title"))
self.setMinimumWidth(500) self.setMinimumWidth(500)
self.setMinimumHeight(400) # Allow more vertical space self.setMinimumHeight(400) # Allow more vertical space
@@ -205,8 +210,8 @@ class PlaylistSelectionDialog(QDialog):
# Top buttons (Select/Deselect All) # Top buttons (Select/Deselect All)
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
select_all_btn = QPushButton("Select All") select_all_btn = QPushButton(_("buttons.select_all"))
deselect_all_btn = QPushButton("Deselect All") deselect_all_btn = QPushButton(_("buttons.deselect_all"))
select_all_btn.clicked.connect(self._select_all) select_all_btn.clicked.connect(self._select_all)
deselect_all_btn.clicked.connect(self._deselect_all) deselect_all_btn.clicked.connect(self._deselect_all)
# Style the buttons to match the subtitle dialog # Style the buttons to match the subtitle dialog
@@ -250,7 +255,9 @@ class PlaylistSelectionDialog(QDialog):
self._populate_list(previously_selected_string) self._populate_list(previously_selected_string)
# Dialog buttons (OK/Cancel) # Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@@ -428,50 +435,50 @@ class SponsorBlockCategoryDialog(QDialog):
# Default SponsorBlock categories with descriptions # Default SponsorBlock categories with descriptions
SPONSORBLOCK_CATEGORIES = { SPONSORBLOCK_CATEGORIES = {
"sponsor": { "sponsor": {
"name": "Sponsor", "name_key": "sponsorblock.sponsor",
"description": "Paid promotion, paid referrals and direct advertisements", "description_key": "sponsorblock.sponsor_desc",
"default": True, "default": True,
}, },
"selfpromo": { "selfpromo": {
"name": "Unpaid/Self Promotion", "name_key": "sponsorblock.selfpromo",
"description": "Unpaid promotion of creators' own content", "description_key": "sponsorblock.selfpromo_desc",
"default": True, "default": True,
}, },
"interaction": { "interaction": {
"name": "Interaction Reminder", "name_key": "sponsorblock.interaction",
"description": "Asking viewers to like, subscribe, or follow social media", "description_key": "sponsorblock.interaction_desc",
"default": True, "default": True,
}, },
"intro": { "intro": {
"name": "Intro", "name_key": "sponsorblock.intro",
"description": "Video introduction that can be skipped", "description_key": "sponsorblock.intro_desc",
"default": False, "default": False,
}, },
"outro": { "outro": {
"name": "Outro/End Cards", "name_key": "sponsorblock.outro",
"description": "Credits or when the video ends", "description_key": "sponsorblock.outro_desc",
"default": False, "default": False,
}, },
"preview": { "preview": {
"name": "Preview/Recap", "name_key": "sponsorblock.preview",
"description": "Quick recap of previous videos or preview of what's coming up", "description_key": "sponsorblock.preview_desc",
"default": False, "default": False,
}, },
"music_offtopic": { "music_offtopic": {
"name": "Non-Music Section", "name_key": "sponsorblock.music_offtopic",
"description": "Only for music videos. Marks non-music sections", "description_key": "sponsorblock.music_offtopic_desc",
"default": False, "default": False,
}, },
"filler": { "filler": {
"name": "Filler Tangent", "name_key": "sponsorblock.filler",
"description": "Tangential scenes added only for filler or humor", "description_key": "sponsorblock.filler_desc",
"default": False, "default": False,
}, },
} }
def __init__(self, previously_selected=None, parent=None) -> None: def __init__(self, previously_selected=None, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("SponsorBlock Categories") self.setWindowTitle(_("dialogs.sponsorblock_categories"))
self.setMinimumWidth(500) self.setMinimumWidth(500)
self.setMinimumHeight(400) self.setMinimumHeight(400)
@@ -489,15 +496,12 @@ class SponsorBlockCategoryDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Title and description # Title and description
title_label = QLabel("SponsorBlock Categories") title_label = QLabel(_("dialogs.sponsorblock_categories"))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
layout.addWidget(title_label) layout.addWidget(title_label)
desc_label = QLabel( desc_label = QLabel(_("dialogs.sponsorblock_description"))
"Select which types of video segments to automatically remove during download.\n"
"SponsorBlock uses community-submitted data to identify these segments."
)
desc_label.setWordWrap(True) desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;") desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
@@ -521,8 +525,8 @@ class SponsorBlockCategoryDialog(QDialog):
category_layout.setContentsMargins(0, 0, 0, 0) category_layout.setContentsMargins(0, 0, 0, 0)
category_layout.setSpacing(2) category_layout.setSpacing(2)
# Create checkbox with just the name # Create checkbox with localized name
checkbox = QCheckBox(category_info["name"]) checkbox = QCheckBox(_(category_info["name_key"]))
checkbox.setProperty("category_id", category_id) checkbox.setProperty("category_id", category_id)
# Determine if this category should be checked # Determine if this category should be checked
@@ -559,8 +563,8 @@ class SponsorBlockCategoryDialog(QDialog):
""" """
) )
# Create description label # Create description label with localized text
desc_label = QLabel(category_info["description"]) desc_label = QLabel(_(category_info["description_key"]))
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;") desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
desc_label.setWordWrap(True) desc_label.setWordWrap(True)
@@ -577,15 +581,15 @@ class SponsorBlockCategoryDialog(QDialog):
# Quick selection buttons # Quick selection buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
select_defaults_btn = QPushButton("Select Defaults") select_defaults_btn = QPushButton(_("buttons.select_defaults"))
select_defaults_btn.clicked.connect(self.select_defaults) select_defaults_btn.clicked.connect(self.select_defaults)
select_defaults_btn.setStyleSheet(self._get_button_style()) select_defaults_btn.setStyleSheet(self._get_button_style())
select_all_btn = QPushButton("Select All") select_all_btn = QPushButton(_("buttons.select_all"))
select_all_btn.clicked.connect(self.select_all) select_all_btn.clicked.connect(self.select_all)
select_all_btn.setStyleSheet(self._get_button_style()) select_all_btn.setStyleSheet(self._get_button_style())
deselect_all_btn = QPushButton("Deselect All") deselect_all_btn = QPushButton(_("buttons.deselect_all"))
deselect_all_btn.clicked.connect(self.deselect_all) deselect_all_btn.clicked.connect(self.deselect_all)
deselect_all_btn.setStyleSheet(self._get_button_style()) deselect_all_btn.setStyleSheet(self._get_button_style())
@@ -597,7 +601,9 @@ class SponsorBlockCategoryDialog(QDialog):
layout.addLayout(button_layout) layout.addLayout(button_layout)
# Dialog buttons # Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@@ -3,11 +3,13 @@ Settings-related dialogs for YTSage application.
Contains dialogs for configuring download settings and auto-update preferences. Contains dialogs for configuring download settings and auto-update preferences.
""" """
import threading
import time import time
from datetime import datetime from datetime import datetime
import requests import requests
from PySide6.QtCore import Qt from packaging import version as version_parser
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QButtonGroup, QButtonGroup,
QCheckBox, QCheckBox,
@@ -25,19 +27,20 @@ from PySide6.QtWidgets import (
QVBoxLayout, QVBoxLayout,
) )
from src.core.ytsage_logging import logger
from src.core.ytsage_utils import ( from src.core.ytsage_utils import (
check_and_update_ytdlp_auto, check_and_update_ytdlp_auto,
get_auto_update_settings, get_auto_update_settings,
get_ytdlp_version, get_ytdlp_version,
update_auto_update_settings, update_auto_update_settings,
) )
from src.utils.ytsage_logger import logger
from src.utils.ytsage_localization import _
class DownloadSettingsDialog(QDialog): class DownloadSettingsDialog(QDialog):
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None: def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Download Settings") self.setWindowTitle(_("settings.title"))
self.setMinimumWidth(450) self.setMinimumWidth(450)
self.setMinimumHeight(400) self.setMinimumHeight(400)
self.current_path = current_path self.current_path = current_path
@@ -161,17 +164,18 @@ class DownloadSettingsDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# --- Download Path Section --- # --- Download Path Section ---
path_group_box = QGroupBox("Download Path") path_group_box = QGroupBox(_("settings.download_path"))
path_layout = QVBoxLayout() path_layout = QVBoxLayout()
self.path_display = QLabel(self.current_path) self.path_display = QLabel(str(self.current_path))
self.path_display.setWordWrap(True) self.path_display.setWordWrap(True)
self.path_display.setStyleSheet( self.path_display.setStyleSheet(
"QLabel { color: #ffffff; padding: 5px; border: 1px solid #1b2021; border-radius: 4px; background-color: #1b2021; }" "QLabel { color: #ffffff; padding: 5px; border: 1px solid #1b2021; border-radius: 4px; background-color: #1b2021; }"
) )
path_layout.addWidget(self.path_display) path_layout.addWidget(self.path_display)
browse_button = QPushButton("Browse...")
browse_button = QPushButton(_("settings.browse"))
browse_button.clicked.connect(self.browse_new_path) browse_button.clicked.connect(self.browse_new_path)
path_layout.addWidget(browse_button) path_layout.addWidget(browse_button)
@@ -179,11 +183,11 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(path_group_box) layout.addWidget(path_group_box)
# --- Speed Limit Section --- # --- Speed Limit Section ---
speed_group_box = QGroupBox("Speed Limit") speed_group_box = QGroupBox(_("settings.speed_limit"))
speed_layout = QHBoxLayout() speed_layout = QHBoxLayout()
self.speed_limit_input = QLineEdit(str(self.current_limit)) self.speed_limit_input = QLineEdit(str(self.current_limit))
self.speed_limit_input.setPlaceholderText("None") self.speed_limit_input.setPlaceholderText(_("settings.speed_limit_placeholder"))
speed_layout.addWidget(self.speed_limit_input) speed_layout.addWidget(self.speed_limit_input)
self.speed_limit_unit = QComboBox() self.speed_limit_unit = QComboBox()
@@ -195,25 +199,25 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(speed_group_box) layout.addWidget(speed_group_box)
# --- Auto-Update yt-dlp Section --- # --- Auto-Update yt-dlp Section ---
auto_update_group_box = QGroupBox("Auto-Update yt-dlp") auto_update_group_box = QGroupBox(_("settings.auto_update_ytdlp"))
auto_update_layout = QVBoxLayout() auto_update_layout = QVBoxLayout()
# Load current auto-update settings # Load current auto-update settings
auto_settings = get_auto_update_settings() auto_settings = get_auto_update_settings()
# Enable/Disable auto-update checkbox # Enable/Disable auto-update checkbox
self.auto_update_enabled = QCheckBox("Enable automatic yt-dlp updates") self.auto_update_enabled = QCheckBox(_("settings.enable_auto_updates"))
self.auto_update_enabled.setChecked(auto_settings["enabled"]) self.auto_update_enabled.setChecked(auto_settings["enabled"])
auto_update_layout.addWidget(self.auto_update_enabled) auto_update_layout.addWidget(self.auto_update_enabled)
# Frequency options # Frequency options
frequency_label = QLabel("Update frequency:") frequency_label = QLabel(_("settings.update_frequency"))
frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;") frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
auto_update_layout.addWidget(frequency_label) auto_update_layout.addWidget(frequency_label)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)") self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton("Check daily") self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton("Check weekly") self.weekly_radio = QRadioButton(_("settings.check_weekly"))
# Set current selection based on saved settings # Set current selection based on saved settings
current_frequency = auto_settings["frequency"] current_frequency = auto_settings["frequency"]
@@ -230,7 +234,7 @@ class DownloadSettingsDialog(QDialog):
# Test update button # Test update button
test_update_layout = QHBoxLayout() test_update_layout = QHBoxLayout()
test_update_button = QPushButton("Check for Updates Now") test_update_button = QPushButton(_("settings.check_updates_now"))
test_update_button.clicked.connect(self.test_update_check) test_update_button.clicked.connect(self.test_update_check)
test_update_layout.addWidget(test_update_button) test_update_layout.addWidget(test_update_button)
test_update_layout.addStretch() test_update_layout.addStretch()
@@ -240,13 +244,15 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(auto_update_group_box) layout.addWidget(auto_update_group_box)
# Dialog buttons (OK/Cancel) # Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
def browse_new_path(self) -> None: def browse_new_path(self) -> None:
new_path = QFileDialog.getExistingDirectory(self, "Select Download Directory", self.current_path) new_path = QFileDialog.getExistingDirectory(self, _("dialogs.select_folder"), str(self.current_path))
if new_path: if new_path:
self.current_path = new_path self.current_path = new_path
self.path_display.setText(self.current_path) self.path_display.setText(self.current_path)
@@ -314,8 +320,8 @@ class DownloadSettingsDialog(QDialog):
if "Error" in current_version: if "Error" in current_version:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
"Update Check", _("settings.update_check_title"),
"Could not determine current yt-dlp version.", _("settings.could_not_determine_version"),
) )
msg_box.exec() msg_box.exec()
return return
@@ -329,27 +335,25 @@ class DownloadSettingsDialog(QDialog):
current_version = current_version.replace("_", ".") current_version = current_version.replace("_", ".")
latest_version = latest_version.replace("_", ".") latest_version = latest_version.replace("_", ".")
from packaging import version as version_parser
if version_parser.parse(latest_version) > version_parser.parse(current_version): if version_parser.parse(latest_version) > version_parser.parse(current_version):
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Update Check", _("settings.update_check_title"),
f"Update available!\n\nCurrent: {current_version}\nLatest: {latest_version}\n\nUse the 'Update yt-dlp' button in the main window to update.", _("settings.update_available_dialog", current=current_version, latest=latest_version),
) )
msg_box.exec() msg_box.exec()
else: else:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Update Check", _("settings.update_check_title"),
f"yt-dlp is up to date!\n\nCurrent version: {current_version}", _("settings.up_to_date_dialog", version=current_version),
) )
msg_box.exec() msg_box.exec()
except Exception as e: except Exception as e:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
"Update Check", _("settings.update_check_title"),
f"Error checking for updates: {str(e)}", _("settings.error_checking_updates", error=str(e)),
) )
msg_box.exec() msg_box.exec()
@@ -375,13 +379,13 @@ class DownloadSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency): if update_auto_update_settings(enabled, frequency):
QMessageBox.information( QMessageBox.information(
self, self,
"Settings Saved", _("settings.settings_saved_title"),
"Auto-update settings have been saved successfully!", _("settings.settings_saved_message"),
) )
else: else:
QMessageBox.warning(self, "Error", "Failed to save auto-update settings.") QMessageBox.warning(self, _("settings.error_title"), _("settings.failed_save_settings"))
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Error", f"Error saving auto-update settings: {str(e)}") QMessageBox.critical(self, _("settings.error_title"), _("settings.error_saving_settings", error=str(e)))
# Call the parent accept method to close the dialog # Call the parent accept method to close the dialog
super().accept() super().accept()
@@ -390,7 +394,7 @@ class DownloadSettingsDialog(QDialog):
class AutoUpdateSettingsDialog(QDialog): class AutoUpdateSettingsDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Auto-Update Settings") self.setWindowTitle(_("settings.auto_update_title"))
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setMinimumHeight(300) self.setMinimumHeight(300)
@@ -406,32 +410,32 @@ class AutoUpdateSettingsDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Title # Title
title_label = QLabel("<h2>🔄 Auto-Update Settings</h2>") title_label = QLabel(f"<h2>{_("settings.auto_update_header")}</h2>")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label) layout.addWidget(title_label)
# Description # Description
desc_label = QLabel("Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.") desc_label = QLabel(_("settings.auto_update_description"))
desc_label.setWordWrap(True) desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;") desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
layout.addWidget(desc_label) layout.addWidget(desc_label)
# Enable/Disable auto-update # Enable/Disable auto-update
self.enable_checkbox = QCheckBox("Enable automatic yt-dlp updates") self.enable_checkbox = QCheckBox(_("settings.enable_auto_updates"))
self.enable_checkbox.setChecked(True) # Default enabled self.enable_checkbox.setChecked(True) # Default enabled
self.enable_checkbox.toggled.connect(self.on_enable_toggled) self.enable_checkbox.toggled.connect(self.on_enable_toggled)
layout.addWidget(self.enable_checkbox) layout.addWidget(self.enable_checkbox)
# Frequency options # Frequency options
frequency_group = QGroupBox("Update Frequency") frequency_group = QGroupBox(_("settings.update_frequency_group"))
frequency_layout = QVBoxLayout() frequency_layout = QVBoxLayout()
self.frequency_group = QButtonGroup(self) self.frequency_group = QButtonGroup(self)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)") self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton("Check daily") self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton("Check weekly") self.weekly_radio = QRadioButton(_("settings.check_weekly"))
self.daily_radio.setChecked(True) # Default to daily self.daily_radio.setChecked(True) # Default to daily
@@ -447,12 +451,12 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(frequency_group) layout.addWidget(frequency_group)
# Current status # Current status
status_group = QGroupBox("Current Status") status_group = QGroupBox(_("settings.current_status"))
status_layout = QVBoxLayout() status_layout = QVBoxLayout()
self.current_version_label = QLabel("Current yt-dlp version: Checking...") self.current_version_label = QLabel(_("settings.current_version_label"))
self.last_check_label = QLabel("Last update check: Never") self.last_check_label = QLabel(_("settings.last_check_label"))
self.next_check_label = QLabel("Next check: Based on settings") self.next_check_label = QLabel(_("settings.next_check_label"))
status_layout.addWidget(self.current_version_label) status_layout.addWidget(self.current_version_label)
status_layout.addWidget(self.last_check_label) status_layout.addWidget(self.last_check_label)
@@ -462,17 +466,17 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(status_group) layout.addWidget(status_group)
# Manual check button # Manual check button
self.manual_check_btn = QPushButton("🔍 Check for Updates Now") self.manual_check_btn = QPushButton(_("settings.manual_check_button"))
self.manual_check_btn.clicked.connect(self.manual_check) self.manual_check_btn.clicked.connect(self.manual_check)
layout.addWidget(self.manual_check_btn) layout.addWidget(self.manual_check_btn)
# Buttons # Buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
self.save_btn = QPushButton("Save Settings") self.save_btn = QPushButton(_("settings.save_settings"))
self.save_btn.clicked.connect(self.save_settings) self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton("Cancel") self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.clicked.connect(self.reject) self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn) button_layout.addWidget(self.save_btn)
@@ -571,9 +575,9 @@ class AutoUpdateSettingsDialog(QDialog):
last_check = settings["last_check"] last_check = settings["last_check"]
if last_check > 0: if last_check > 0:
last_check_time = datetime.fromtimestamp(last_check).strftime("%Y-%m-%d %H:%M:%S") last_check_time = datetime.fromtimestamp(last_check).strftime("%Y-%m-%d %H:%M:%S")
self.last_check_label.setText(f"Last update check: {last_check_time}") self.last_check_label.setText(_("auto_update.last_check", time=last_check_time))
else: else:
self.last_check_label.setText("Last update check: Never") self.last_check_label.setText(_("auto_update.last_check_never"))
# Calculate next check time # Calculate next check time
self.update_next_check_label() self.update_next_check_label()
@@ -582,13 +586,13 @@ class AutoUpdateSettingsDialog(QDialog):
self.on_enable_toggled(settings["enabled"]) self.on_enable_toggled(settings["enabled"])
except Exception as e: except Exception as e:
logger.error(f"Error loading auto-update settings: {e}") logger.exception(f"Error loading auto-update settings: {e}")
def update_next_check_label(self) -> None: def update_next_check_label(self) -> None:
"""Update the next check label based on current settings.""" """Update the next check label based on current settings."""
try: try:
if not self.enable_checkbox.isChecked(): if not self.enable_checkbox.isChecked():
self.next_check_label.setText("Next check: Disabled") self.next_check_label.setText(_("auto_update.next_check_disabled"))
return return
settings = get_auto_update_settings() settings = get_auto_update_settings()
@@ -596,7 +600,7 @@ class AutoUpdateSettingsDialog(QDialog):
frequency = self.get_selected_frequency() frequency = self.get_selected_frequency()
if last_check == 0: if last_check == 0:
self.next_check_label.setText("Next check: On next startup") self.next_check_label.setText(_("auto_update.next_check_startup"))
return return
next_check_time = last_check next_check_time = last_check
@@ -609,14 +613,14 @@ class AutoUpdateSettingsDialog(QDialog):
current_time = time.time() current_time = time.time()
if next_check_time <= current_time: if next_check_time <= current_time:
self.next_check_label.setText("Next check: Now (overdue)") self.next_check_label.setText(_("auto_update.next_check_overdue"))
else: else:
next_check_datetime = datetime.fromtimestamp(next_check_time) next_check_datetime = datetime.fromtimestamp(next_check_time)
self.next_check_label.setText(f"Next check: {next_check_datetime.strftime('%Y-%m-%d %H:%M:%S')}") self.next_check_label.setText(_("auto_update.next_check", time=next_check_datetime.strftime('%Y-%m-%d %H:%M:%S')))
except Exception as e: except Exception as e:
self.next_check_label.setText("Next check: Error calculating") self.next_check_label.setText(_("auto_update.next_check_error"))
logger.error(f"Error calculating next check time: {e}") logger.exception(f"Error calculating next check time: {e}")
def on_enable_toggled(self, enabled) -> None: def on_enable_toggled(self, enabled) -> None:
"""Handle enable/disable checkbox toggle.""" """Handle enable/disable checkbox toggle."""
@@ -638,7 +642,7 @@ class AutoUpdateSettingsDialog(QDialog):
def manual_check(self) -> None: def manual_check(self) -> None:
"""Perform a manual update check.""" """Perform a manual update check."""
self.manual_check_btn.setEnabled(False) self.manual_check_btn.setEnabled(False)
self.manual_check_btn.setText("🔄 Checking...") self.manual_check_btn.setText(_("auto_update.checking"))
# Force an immediate update check # Force an immediate update check
def check_in_thread() -> None: def check_in_thread() -> None:
@@ -646,16 +650,12 @@ class AutoUpdateSettingsDialog(QDialog):
result = check_and_update_ytdlp_auto() result = check_and_update_ytdlp_auto()
# Update UI in main thread # Update UI in main thread
from PySide6.QtCore import QTimer
QTimer.singleShot(0, lambda: self.manual_check_finished(result)) QTimer.singleShot(0, lambda: self.manual_check_finished(result))
except Exception as e: except Exception as e:
logger.error(f"Error during manual check: {e}") logger.exception(f"Error during manual check: {e}")
QTimer.singleShot(0, lambda: self.manual_check_finished(False)) QTimer.singleShot(0, lambda: self.manual_check_finished(False))
# Run in separate thread to avoid blocking UI # Run in separate thread to avoid blocking UI
import threading
threading.Thread(target=check_in_thread, daemon=True).start() threading.Thread(target=check_in_thread, daemon=True).start()
def _create_styled_message_box(self, icon, title, text) -> QMessageBox: def _create_styled_message_box(self, icon, title, text) -> QMessageBox:
@@ -696,7 +696,7 @@ class AutoUpdateSettingsDialog(QDialog):
def manual_check_finished(self, success) -> None: def manual_check_finished(self, success) -> None:
"""Handle completion of manual update check.""" """Handle completion of manual update check."""
self.manual_check_btn.setEnabled(True) self.manual_check_btn.setEnabled(True)
self.manual_check_btn.setText("🔍 Check for Updates Now") self.manual_check_btn.setText(_("auto_update.check_now"))
if success: if success:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
@@ -725,19 +725,19 @@ class AutoUpdateSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency): if update_auto_update_settings(enabled, frequency):
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Settings Saved", _("settings.settings_saved_title"),
"✅ Auto-update settings have been saved successfully!", _("settings.settings_saved_successfully"),
) )
msg_box.exec() msg_box.exec()
self.accept() self.accept()
else: else:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
"Error", _("settings.error_title"),
"❌ Failed to save auto-update settings.\nPlease try again.", _("settings.failed_save_settings"),
) )
msg_box.exec() msg_box.exec()
except Exception as e: except Exception as e:
logger.error(f"Error saving auto-update settings: {e}") logger.exception(f"Error saving auto-update settings: {e}")
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, "Error", f"❌ Error saving settings: {str(e)}") msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, _("settings.error_title"), _("settings", "error_saving", error=str(e)))
msg_box.exec() msg_box.exec()
@@ -7,6 +7,7 @@ import os
import subprocess import subprocess
import sys import sys
import time import time
from importlib.metadata import PackageNotFoundError
from pathlib import Path from pathlib import Path
import requests import requests
@@ -14,14 +15,19 @@ from packaging import version
from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtCore import Qt, QThread, QTimer, Signal
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout
from src.core.ytsage_logging import logger
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config
from src.core.ytsage_yt_dlp import get_yt_dlp_path from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
from src.utils.ytsage_localization import LocalizationManager
# Shorthand for localization
_ = LocalizationManager.get_text
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
try: try:
from importlib.metadata import version as importlib_version
from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError from importlib.metadata import PackageNotFoundError as ImportlibPackageNotFoundError
from importlib.metadata import version as importlib_version
def get_version(package_name: str) -> str: def get_version(package_name: str) -> str:
return importlib_version(package_name) return importlib_version(package_name)
@@ -30,8 +36,10 @@ try:
except ImportError: except ImportError:
# Fallback for older Python versions # Fallback for older Python versions
import pkg_resources import pkg_resources
def get_version(package_name: str) -> str: def get_version(package_name: str) -> str:
return pkg_resources.get_distribution(package_name).version return pkg_resources.get_distribution(package_name).version
PackageNotFoundError = pkg_resources.DistributionNotFound PackageNotFoundError = pkg_resources.DistributionNotFound
try: try:
@@ -71,7 +79,7 @@ class VersionCheckThread(QThread):
else: else:
error_message = "yt-dlp not available." error_message = "yt-dlp not available."
self.finished.emit(current_version, latest_version, error_message) self.finished.emit(current_version, latest_version, error_message)
return return
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
# Try fallback if timeout # Try fallback if timeout
if YT_DLP_AVAILABLE: if YT_DLP_AVAILABLE:
@@ -115,16 +123,16 @@ class UpdateThread(QThread):
error_message = "" error_message = ""
success = False success = False
try: try:
self.update_status.emit("🔍 Checking current installation...") self.update_status.emit(_('update.checking_current'))
self.update_progress.emit(10) self.update_progress.emit(10)
# Get the yt-dlp path # Get the yt-dlp path
try: try:
yt_dlp_path = get_yt_dlp_path() yt_dlp_path = get_yt_dlp_path()
self.update_status.emit(f"📍 Found yt-dlp at: {yt_dlp_path}") self.update_status.emit(_('update.found_at', path=yt_dlp_path))
except Exception as e: except Exception as e:
self.update_status.emit(f"❌ Error getting yt-dlp path: {e}") self.update_status.emit(_('update.error_getting_path', error=e))
self.update_finished.emit(False, f"❌ Error getting yt-dlp path: {e}") self.update_finished.emit(False, _('update.error_getting_path', error=e))
return return
# Extra logic moved to src\utils\ytsage_constants.py # Extra logic moved to src\utils\ytsage_constants.py
@@ -149,24 +157,24 @@ class UpdateThread(QThread):
is_app_managed = False is_app_managed = False
if is_app_managed: if is_app_managed:
self.update_status.emit("📦 Updating app-managed yt-dlp binary...") self.update_status.emit(_('update.updating_binary'))
success = self._update_binary(yt_dlp_path) success = self._update_binary(yt_dlp_path)
else: else:
self.update_status.emit("🐍 Updating system yt-dlp via pip...") self.update_status.emit(_('update.updating_pip'))
success = self._update_via_pip() success = self._update_via_pip()
if success: if success:
self.update_progress.emit(100) self.update_progress.emit(100)
error_message = "✅ yt-dlp has been successfully updated!" error_message = _('update.update_success')
else: else:
error_message = "❌ Failed to update yt-dlp. Please try again or check your internet connection." error_message = _('update.update_failed')
except requests.RequestException as e: except requests.RequestException as e:
error_message = f"❌ Network error during update: {str(e)}" error_message = _('update.network_error', error=e)
self.update_status.emit(error_message) self.update_status.emit(error_message)
success = False success = False
except Exception as e: except Exception as e:
error_message = f"❌ Update failed: {str(e)}" error_message = _('update.general_error', error=e)
self.update_status.emit(error_message) self.update_status.emit(error_message)
success = False success = False
@@ -193,61 +201,61 @@ class UpdateThread(QThread):
logger.info("UpdateThread: yt-dlp update completed successfully.") logger.info("UpdateThread: yt-dlp update completed successfully.")
if result.stdout: if result.stdout:
logger.debug(f"yt-dlp output: {result.stdout.strip()}") logger.debug(f"yt-dlp output: {result.stdout.strip()}")
self.update_status.emit("✅ Binary successfully updated!") self.update_status.emit(_('update.binary_updated'))
self.update_progress.emit(95) self.update_progress.emit(95)
return True return True
else: else:
logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}") logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}")
self.update_status.emit(f"❌ yt-dlp update failed: {result.stderr.strip()}") self.update_status.emit(_('update.update_failed_stderr', error=result.stderr.strip()))
return False return False
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.error("UpdateThread: yt-dlp update timed out.") logger.error("UpdateThread: yt-dlp update timed out.")
self.update_status.emit("❌ yt-dlp update timed out.") self.update_status.emit(_('update.update_timeout'))
return False return False
except Exception as e: except Exception as e:
logger.error(f"UpdateThread: Unexpected error during update: {e}", exc_info=True) logger.exception(f"UpdateThread: Unexpected error during update: {e}")
self.update_status.emit(f"❌ Unexpected error during update: {e}") self.update_status.emit(_('update.unexpected_error', error=e))
return False return False
def _update_via_pip(self) -> bool: def _update_via_pip(self) -> bool:
"""Update yt-dlp via pip.""" """Update yt-dlp via pip."""
try: try:
self.update_status.emit("🔍 Checking current pip installation...") self.update_status.emit(_('update.checking_pip'))
self.update_progress.emit(30) self.update_progress.emit(30)
# Get current version # Get current version
try: try:
current_version = get_version("yt-dlp") current_version = get_version("yt-dlp")
self.update_status.emit(f"📋 Current version: {current_version}") self.update_status.emit(_('update.current_version', version=current_version))
except PackageNotFoundError: except PackageNotFoundError:
self.update_status.emit("⚠️ yt-dlp not found via pip, attempting installation...") self.update_status.emit(_('update.not_found_pip'))
current_version = "0.0.0" current_version = "0.0.0"
self.update_progress.emit(40) self.update_progress.emit(40)
# Get the latest version from PyPI # Get the latest version from PyPI
self.update_status.emit("🌐 Checking for latest version...") self.update_status.emit(_('update.checking_latest'))
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10) response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
if response.status_code != 200: if response.status_code != 200:
self.update_status.emit("❌ Failed to check for updates") self.update_status.emit(_('update.failed_check_updates'))
return False return False
data = response.json() data = response.json()
latest_version = data["info"]["version"] latest_version = data["info"]["version"]
self.update_status.emit(f"🆕 Latest version: {latest_version}") self.update_status.emit(_('update.latest_version', version=latest_version))
self.update_progress.emit(50) self.update_progress.emit(50)
# Compare versions # Compare versions
if version.parse(latest_version) > version.parse(current_version): if version.parse(latest_version) > version.parse(current_version):
self.update_status.emit(f"⬆️ Updating from {current_version} to {latest_version}...") self.update_status.emit(_('update.updating_from_to', current=current_version, latest=latest_version))
self.update_progress.emit(60) self.update_progress.emit(60)
try: try:
# Run pip update with timeout # Run pip update with timeout
self.update_status.emit("📦 Running pip install --upgrade...") self.update_status.emit(_('update.running_pip_install'))
update_result = subprocess.run( update_result = subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"], [sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
capture_output=True, capture_output=True,
@@ -260,33 +268,33 @@ class UpdateThread(QThread):
self.update_progress.emit(85) self.update_progress.emit(85)
if update_result.returncode == 0: if update_result.returncode == 0:
self.update_status.emit("✅ Pip update completed successfully!") self.update_status.emit(_('update.pip_completed'))
self.update_progress.emit(95) self.update_progress.emit(95)
return True return True
else: else:
self.update_status.emit(f"❌ Pip update failed: {update_result.stderr}") self.update_status.emit(_('update.pip_failed', error=update_result.stderr))
return False return False
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self.update_status.emit("❌ Pip update timed out after 5 minutes") self.update_status.emit(_("update.pip_timeout"))
return False return False
except Exception as e: except Exception as e:
self.update_status.emit(f"❌ Error during pip update: {e}") self.update_status.emit(_('update.error_pip_update', error=e))
return False return False
else: else:
self.update_status.emit("✅ yt-dlp is already up to date!") self.update_status.emit(_("update.already_up_to_date"))
self.update_progress.emit(95) self.update_progress.emit(95)
return True return True
except Exception as e: except Exception as e:
self.update_status.emit(f"❌ Pip update failed: {e}") self.update_status.emit(_('update.pip_update_failed', error=e))
return False return False
class YTDLPUpdateDialog(QDialog): class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Update yt-dlp") self.setWindowTitle(_('update.title'))
self.setMinimumWidth(450) self.setMinimumWidth(450)
self.setMinimumHeight(200) self.setMinimumHeight(200)
self._closing = False # Flag to track if dialog is closing self._closing = False # Flag to track if dialog is closing
@@ -294,7 +302,7 @@ class YTDLPUpdateDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Status label # Status label
self.status_label = QLabel("Checking for updates...") self.status_label = QLabel(_('update.checking'))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setWordWrap(True) self.status_label.setWordWrap(True)
self.status_label.setMinimumHeight(60) self.status_label.setMinimumHeight(60)
@@ -307,11 +315,11 @@ class YTDLPUpdateDialog(QDialog):
# Buttons # Buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
self.update_btn = QPushButton("Update") self.update_btn = QPushButton(_('buttons.update'))
self.update_btn.clicked.connect(self.perform_update) self.update_btn.clicked.connect(self.perform_update)
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
self.close_btn = QPushButton("Close") self.close_btn = QPushButton(_('buttons.close'))
self.close_btn.clicked.connect(self.close) self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.update_btn) button_layout.addWidget(self.update_btn)
@@ -367,7 +375,7 @@ class YTDLPUpdateDialog(QDialog):
self.check_version() self.check_version()
def check_version(self) -> None: def check_version(self) -> None:
self.status_label.setText("Checking for updates...") self.status_label.setText(_('update.checking'))
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
self.version_check_thread = VersionCheckThread() self.version_check_thread = VersionCheckThread()
self.version_check_thread.finished.connect(self.on_version_check_finished) self.version_check_thread.finished.connect(self.on_version_check_finished)
@@ -384,7 +392,7 @@ class YTDLPUpdateDialog(QDialog):
return return
if not current_version or not latest_version: if not current_version or not latest_version:
self.status_label.setText("Could not determine versions.") self.status_label.setText(_('update.could_not_determine'))
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
return return
@@ -395,32 +403,32 @@ class YTDLPUpdateDialog(QDialog):
if current_ver < latest_ver: if current_ver < latest_ver:
self.status_label.setText( self.status_label.setText(
f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}" _('update.update_available', current=current_version, latest=latest_version)
) )
self.update_btn.setEnabled(True) self.update_btn.setEnabled(True)
else: else:
self.status_label.setText(f"yt-dlp is up to date (version {current_version})") self.status_label.setText(_('update.already_latest', version=current_version))
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
except version.InvalidVersion: except version.InvalidVersion:
# If version parsing fails, do a simple string comparison # If version parsing fails, do a simple string comparison
if current_version != latest_version: if current_version != latest_version:
self.status_label.setText( self.status_label.setText(
f"Update available! (Comparison failed)\nCurrent: {current_version}\nLatest: {latest_version}" _('update.update_available_failed', current=current_version, latest=latest_version)
) )
self.update_btn.setEnabled(True) self.update_btn.setEnabled(True)
else: else:
self.status_label.setText(f"yt-dlp is up to date (version {current_version})") self.status_label.setText(_('update.up_to_date', version=current_version))
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
except Exception as e: except Exception as e:
self.status_label.setText(f"Error comparing versions: {e}") self.status_label.setText(_('update.error_comparing', error=e))
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
def perform_update(self) -> None: def perform_update(self) -> None:
# Immediate visual feedback # Immediate visual feedback
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
self.close_btn.setEnabled(False) self.close_btn.setEnabled(False)
self.update_btn.setText("Updating...") self.update_btn.setText(_('update.updating'))
self.status_label.setText("🚀 Initializing update process...") self.status_label.setText(_('update.initializing'))
# Show progress bar immediately # Show progress bar immediately
self.progress_bar.setRange(0, 100) self.progress_bar.setRange(0, 100)
@@ -458,7 +466,7 @@ class YTDLPUpdateDialog(QDialog):
self.progress_bar.setValue(100) self.progress_bar.setValue(100)
self.status_label.setText(message) self.status_label.setText(message)
self.close_btn.setEnabled(True) self.close_btn.setEnabled(True)
self.update_btn.setText("Update") # Reset button text self.update_btn.setText(_('buttons.update')) # Reset button text
if success: if success:
# Show success briefly then auto-check version # Show success briefly then auto-check version
@@ -553,10 +561,7 @@ class AutoUpdateThread(QThread):
logger.warning(f"AutoUpdateThread: Network error during auto-update check: {e}") logger.warning(f"AutoUpdateThread: Network error during auto-update check: {e}")
self.update_finished.emit(False, f"Network error: {e}") self.update_finished.emit(False, f"Network error: {e}")
except Exception as e: except Exception as e:
logger.error( logger.exception(f"AutoUpdateThread: Error during auto-update check: {e}", exc_info=True)
f"AutoUpdateThread: Error during auto-update check: {e}",
exc_info=True,
)
self.update_finished.emit(False, f"Update check error: {e}") self.update_finished.emit(False, f"Update check error: {e}")
except Exception as e: except Exception as e:
@@ -595,7 +600,7 @@ class AutoUpdateThread(QThread):
return self._update_via_pip() return self._update_via_pip()
except Exception as e: except Exception as e:
logger.error(f"AutoUpdateThread: Error in _perform_update: {e}", exc_info=True) logger.exception(f"AutoUpdateThread: Error in _perform_update: {e}")
return False return False
def _update_binary(self, yt_dlp_path: Path) -> bool: def _update_binary(self, yt_dlp_path: Path) -> bool:
@@ -629,7 +634,7 @@ class AutoUpdateThread(QThread):
return False return False
except Exception as e: except Exception as e:
logger.error(f"AutoUpdateThread: Unexpected error during update: {e}", exc_info=True) logger.exception(f"AutoUpdateThread: Unexpected error during update: {e}")
return False return False
def _update_via_pip(self) -> bool: def _update_via_pip(self) -> bool:
@@ -684,5 +689,5 @@ class AutoUpdateThread(QThread):
return True return True
except Exception as e: except Exception as e:
logger.error(f"AutoUpdateThread: Pip update failed: {e}", exc_info=True) logger.exception(f"AutoUpdateThread: Pip update failed: {e}")
return False return False
+221 -167
View File
@@ -1,58 +1,99 @@
from typing import TYPE_CHECKING, cast
from PySide6.QtCore import QObject, Qt, Signal from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtGui import QColor from PySide6.QtGui import QColor, QFontMetrics
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget
from src.utils.ytsage_localization import _
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp
class FormatSignals(QObject): class FormatSignals(QObject):
format_update = Signal(list) format_update = Signal(list)
class FormatTableMixin: class FormatTableMixin:
def setup_format_table(self) -> QTableWidget: def _calculate_column_width(self, label: str, min_width: int, padding: int) -> int:
self.format_signals = FormatSignals() """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 _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}, # Select - needs space for checkbox
{"min_width": 100, "padding": 30}, # Quality
{"min_width": 85, "padding": 30}, # Extension
{"min_width": 100, "padding": 30}, # Resolution
{"min_width": 90, "padding": 30}, # File Size
{"min_width": 100, "padding": 30}, # Codec
{"min_width": 100, "padding": 30}, # Audio
{"min_width": 60, "padding": 30}, # FPS
{"min_width": 60, "padding": 30}, # HDR
]
for col_index, (label, config) in enumerate(zip(header_labels, configs)):
calculated_width = self._calculate_column_width(label, config["min_width"], config["padding"])
if col_index < 8: # All columns except the last one are fixed
self.format_table.horizontalHeader().setSectionResizeMode(col_index, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(col_index, calculated_width)
else: # HDR column stretches to fill remaining space
self.format_table.horizontalHeader().setSectionResizeMode(col_index, QHeaderView.ResizeMode.Stretch)
def setup_format_table(self) -> QTableWidget:
self = cast("YTSageApp", self) # for autocompletion and type inference.
self.format_signals = FormatSignals()
# Format table with improved styling # Format table with improved styling
self.format_table = QTableWidget() self.format_table = QTableWidget()
self.format_table.setColumnCount(8) self.format_table.setColumnCount(9)
self.format_table.setHorizontalHeaderLabels(
[ # Get translated header labels
"Select", header_labels = [
"Quality", _("formats.select"),
"Extension", _("formats.quality"),
"Resolution", _("formats.extension"),
"File Size", _("formats.resolution"),
"Codec", _("formats.file_size"),
"Audio", _("formats.codec"),
"Notes", _("formats.audio"),
] _("formats.fps"),
) _("formats.hdr"),
]
self.format_table.setHorizontalHeaderLabels(header_labels)
# Enable alternating row colors # Enable alternating row colors
self.format_table.setAlternatingRowColors(True) self.format_table.setAlternatingRowColors(True)
# Set specific column widths and resize modes # Apply responsive column widths
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # Select self._apply_column_widths(header_labels, is_playlist_mode=False)
self.format_table.setColumnWidth(0, 50) # Select column width
self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) # Quality
self.format_table.setColumnWidth(1, 100) # Quality width
self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) # Extension
self.format_table.setColumnWidth(2, 80) # Extension width
self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Resolution
self.format_table.setColumnWidth(3, 100) # Resolution width
self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # File Size
self.format_table.setColumnWidth(4, 100) # File Size width
self.format_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) # Codec
self.format_table.setColumnWidth(5, 150) # Codec width
self.format_table.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed) # Audio
self.format_table.setColumnWidth(6, 120) # Audio width
self.format_table.horizontalHeader().setSectionResizeMode(7, QHeaderView.ResizeMode.Stretch) # Notes (will stretch)
# Set vertical header (row numbers) visible to false # Set vertical header (row numbers) visible to false
self.format_table.verticalHeader().setVisible(False) self.format_table.verticalHeader().setVisible(False)
@@ -124,6 +165,8 @@ class FormatTableMixin:
return self.format_table return self.format_table
def filter_formats(self) -> None: def filter_formats(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
if not hasattr(self, "all_formats"): if not hasattr(self, "all_formats"):
return return
@@ -151,10 +194,13 @@ class FormatTableMixin:
# Sort formats by quality # Sort formats by quality
def get_quality(f): def get_quality(f):
if f.get("vcodec") != "none": if f.get("vcodec") != "none":
res = f.get("resolution", "0x0").split("x")[-1] resolution = f.get("resolution", "0x0")
if resolution is None or not isinstance(resolution, str):
return 0
try: try:
res = resolution.split("x")[-1]
return int(res) return int(res)
except ValueError: except (ValueError, IndexError):
return 0 return 0
else: else:
return f.get("abr", 0) return f.get("abr", 0)
@@ -165,6 +211,8 @@ class FormatTableMixin:
self.format_signals.format_update.emit(filtered_formats) self.format_signals.format_update.emit(filtered_formats)
def _update_format_table(self, formats) -> None: def _update_format_table(self, formats) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
self.format_table.setRowCount(0) self.format_table.setRowCount(0)
self.format_checkboxes.clear() self.format_checkboxes.clear()
@@ -172,64 +220,40 @@ class FormatTableMixin:
# Configure columns based on mode # Configure columns based on mode
if is_playlist_mode: if is_playlist_mode:
self.format_table.setColumnCount(5) self.format_table.setColumnCount(6)
self.format_table.setHorizontalHeaderLabels(["Select", "Quality", "Resolution", "Notes", "Audio"]) 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 # Configure column visibility and resizing for playlist mode
self.format_table.setColumnHidden(5, True)
self.format_table.setColumnHidden(6, True) self.format_table.setColumnHidden(6, True)
self.format_table.setColumnHidden(7, True) self.format_table.setColumnHidden(7, True)
self.format_table.setColumnHidden(8, True)
# Set specific resize modes for playlist columns # Apply responsive column widths for playlist mode
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) self._apply_column_widths(header_labels, is_playlist_mode=True)
self.format_table.setColumnWidth(0, 50)
self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
else: else:
self.format_table.setColumnCount(8) self.format_table.setColumnCount(9)
self.format_table.setHorizontalHeaderLabels( header_labels = [
[ _("formats.select"),
"Select", _("formats.quality"),
"Quality", _("formats.extension"),
"Extension", _("formats.resolution"),
"Resolution", _("formats.file_size"),
"File Size", _("formats.codec"),
"Codec", _("formats.audio"),
"Audio", _("formats.fps"),
"Notes", _("formats.hdr"),
] ]
) self.format_table.setHorizontalHeaderLabels(header_labels)
# Ensure all columns are visible # Ensure all columns are visible
for i in range(2, 8): for i in range(2, 9):
self.format_table.setColumnHidden(i, False) self.format_table.setColumnHidden(i, False)
# Reapply resize modes for non-playlist mode if needed (optional, might be okay without) # Apply responsive column widths for normal mode
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) self._apply_column_widths(header_labels, is_playlist_mode=False)
self.format_table.setColumnWidth(0, 50)
self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(1, 100)
self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(2, 80)
self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(3, 100)
self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(4, 100)
self.format_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(5, 150)
self.format_table.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed)
self.format_table.setColumnWidth(6, 120)
self.format_table.horizontalHeader().setSectionResizeMode(7, QHeaderView.ResizeMode.Stretch)
# Find best quality format for recommendations (only needed for non-playlist mode notes)
best_video_size = 0
if not is_playlist_mode:
best_video_size = max(
(f.get("filesize", 0) for f in formats if f.get("vcodec") != "none"),
default=0,
)
for f in formats: for f in formats:
row = self.format_table.rowCount() row = self.format_table.rowCount()
@@ -252,14 +276,15 @@ class FormatTableMixin:
# Column 1: Quality (Always shown) # Column 1: Quality (Always shown)
quality_text = self.get_quality_label(f) quality_text = self.get_quality_label(f)
quality_item = QTableWidgetItem(quality_text) quality_item = QTableWidgetItem(quality_text)
# Set color based on quality # Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms)
if "Best" in quality_text: quality_lower = quality_text.lower() # Make comparison case-insensitive
if any(term.lower() in quality_lower for term in ["Best", "Óptima", "Mejor", "Melhor", "Лучшее", "最佳", "Beste", "Meilleure", "सर्वोत्तम", "Terbaik", "En iyi", "Najlepsza", "Najlepszy", "Najlepsze", "Migliore", "Miglior", "الأفضل", "أفضل", "最高"]):
quality_item.setForeground(QColor("#00ff00")) # Green for best quality quality_item.setForeground(QColor("#00ff00")) # Green for best quality
elif "High" in quality_text: elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "Áudio Alto", "Audio Alto", "Высокое", "高清", "高质量", "Hoch", "Haute", "Élevé", "Audio élevé", "उच्च", "उच्च ऑडियो", "Tinggi", "Audio tinggi", "Yüksek", "Yüksek ses", "Wysoka", "Wysoki", "Wysokie", "Alta", "Audio alto", "عالية", "عالي", "صوت عالي", "", "高音質"]):
quality_item.setForeground(QColor("#00cc00")) # Light green for high quality quality_item.setForeground(QColor("#00cc00")) # Light green for high quality
elif "Medium" in quality_text: elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "Média", "Áudio Médio", "Audio Medio", "Среднее", "中等", "Mittel", "Moyenne", "Audio moyen", "मध्यम", "मध्यम ऑडियो", "Sedang", "Audio sedang", "Orta", "Orta ses", "Średnia", "Średni", "Średnie", "Media", "Audio medio", "متوسطة", "متوسط", "صوت متوسط", "", "中音質"]):
quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality
elif "Low" in quality_text: elif any(term.lower() in quality_lower for term in ["Low", "Baja", "Bajo", "Baixa", "Áudio Baixo", "Audio Bajo", "Низкое", "低质量", "Niedrig", "Niedriges Audio", "Faible", "Audio faible", "Qualité faible", "निम्न", "निम्न ऑडियो", "निम्न गुणवत्ता", "Rendah", "Audio rendah", "Kualitas rendah", "Düşük", "Düşük ses", "Düşük kalite", "Niska", "Niski", "Niskie", "Bassa", "Audio basso", "Bassa qualità", "منخفضة", "منخفض", "صوت منخفض", "جودة منخفضة", "", "低音質", "低品質"]):
quality_item.setForeground(QColor("#ff5555")) # Red for low quality quality_item.setForeground(QColor("#ff5555")) # Red for low quality
self.format_table.setItem(row, 1, quality_item) self.format_table.setItem(row, 1, quality_item)
@@ -268,37 +293,67 @@ class FormatTableMixin:
# Column 2: Resolution (Always shown) # Column 2: Resolution (Always shown)
resolution = f.get("resolution", "N/A") resolution = f.get("resolution", "N/A")
if f.get("vcodec") == "none": if f.get("vcodec") == "none":
resolution = "Audio only" resolution = _("formats.audio_only_resolution")
self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
# Column 3: Notes for playlist mode, Extension for normal mode # Column 3: FPS for playlist mode, Extension for normal mode
if is_playlist_mode: if is_playlist_mode:
# Get notes for playlist mode # Get FPS for playlist mode
notes = self._get_format_notes(f) fps_value = f.get("fps")
notes_item = QTableWidgetItem(notes) if fps_value is not None:
if "✨ Recommended" in notes: # Format FPS value appropriately
notes_item.setForeground(QColor("#00ff00")) # Green for recommended if fps_value >= 1:
elif "💾 Storage friendly" in notes: fps_text = f"{fps_value:.0f}fps"
notes_item.setForeground(QColor("#00ccff")) # Blue for storage friendly else:
elif "📱 Mobile friendly" in notes: fps_text = "N/A" # Very low fps like storyboards
notes_item.setForeground(QColor("#ff9900")) # Orange for mobile else:
self.format_table.setItem(row, 3, notes_item) fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text)
# Color code based on FPS value
if fps_value and fps_value >= 60:
fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps
elif fps_value and fps_value >= 30:
fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps
elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555")) # Red for low fps
else:
fps_item.setForeground(QColor("#888888")) # Gray for N/A
self.format_table.setItem(row, 3, fps_item)
# Column 4: HDR for playlist mode
if f.get("vcodec") == "none":
# Audio-only formats don't have HDR
hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for N/A
else:
hdr_value = f.get("dynamic_range")
if hdr_value and hdr_value != "SDR":
hdr_text = hdr_value
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR
else:
hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for SDR
self.format_table.setItem(row, 4, hdr_item)
else: else:
# Extension for normal mode (column 2) # Extension for normal mode (column 2)
self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper())) self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper()))
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status # Column 4 in playlist mode, Column 6 in normal mode: Audio Status
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge
audio_status = "Will merge audio" if needs_audio else ("✓ Has Audio" if f.get("vcodec") != "none" else "Audio Only") audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only"))
audio_item = QTableWidgetItem(audio_status) audio_item = QTableWidgetItem(audio_status)
if needs_audio: if needs_audio:
audio_item.setForeground(QColor("#ffa500")) audio_item.setForeground(QColor("#ffa500"))
elif audio_status == "Audio Only": elif audio_status == _("formats.audio_only"):
audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only
else: # Has Audio (Video+Audio) else: # Has Audio (Video+Audio)
audio_item.setForeground(QColor("#00cc00")) # Green for included audio audio_item.setForeground(QColor("#00cc00")) # Green for included audio
# Set item for correct column based on mode # Set item for correct column based on mode
audio_column_index = 4 if is_playlist_mode else 6 audio_column_index = 5 if is_playlist_mode else 6
self.format_table.setItem(row, audio_column_index, audio_item) self.format_table.setItem(row, audio_column_index, audio_item)
# --- Populate columns only shown in non-playlist mode --- # --- Populate columns only shown in non-playlist mode ---
@@ -319,45 +374,83 @@ class FormatTableMixin:
codec += f" / {f.get('acodec', 'N/A')}" codec += f" / {f.get('acodec', 'N/A')}"
self.format_table.setItem(row, 5, QTableWidgetItem(codec)) self.format_table.setItem(row, 5, QTableWidgetItem(codec))
# Column 7: Notes # Column 7: FPS (Frame Rate)
notes = self._get_format_notes(f) fps_value = f.get("fps")
notes_item = QTableWidgetItem(notes) if fps_value is not None:
if "✨ Recommended" in notes: # Format FPS value appropriately
notes_item.setForeground(QColor("#00ff00")) # Green for recommended if fps_value >= 1:
elif "💾 Storage friendly" in notes: fps_text = f"{fps_value:.0f}fps"
notes_item.setForeground(QColor("#00ccff")) # Blue for storage friendly else:
elif "📱 Mobile friendly" in notes: fps_text = "N/A" # Very low fps like storyboards
notes_item.setForeground(QColor("#ff9900")) # Orange for mobile else:
self.format_table.setItem(row, 7, notes_item) fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text)
# Color code based on FPS value
if fps_value and fps_value >= 60:
fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps
elif fps_value and fps_value >= 30:
fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps
elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555")) # Red for low fps
else:
fps_item.setForeground(QColor("#888888")) # Gray for N/A
self.format_table.setItem(row, 7, fps_item)
# Column 8: HDR (Dynamic Range)
if f.get("vcodec") == "none":
# Audio-only formats don't have HDR
hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for N/A
else:
hdr_value = f.get("dynamic_range")
if hdr_value and hdr_value != "SDR":
hdr_text = hdr_value
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR
else:
hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for SDR
self.format_table.setItem(row, 8, hdr_item)
def handle_checkbox_click(self, clicked_checkbox) -> None: def handle_checkbox_click(self, clicked_checkbox) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
for checkbox in self.format_checkboxes: for checkbox in self.format_checkboxes:
if checkbox != clicked_checkbox: if checkbox != clicked_checkbox:
checkbox.setChecked(False) checkbox.setChecked(False)
def get_selected_format(self): def get_selected_format(self):
self = cast("YTSageApp", self) # for autocompletion and type inference.
for checkbox in self.format_checkboxes: for checkbox in self.format_checkboxes:
if checkbox.isChecked(): if checkbox.isChecked():
return checkbox.format_id return checkbox.format_id
return None return None
def update_format_table(self, formats) -> None: def update_format_table(self, formats) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
self.all_formats = formats self.all_formats = formats
self.format_signals.format_update.emit(formats) self.format_signals.format_update.emit(formats)
def get_quality_label(self, format_info) -> str: def get_quality_label(self, format_info) -> str:
"""Determine quality label based on format information""" """Determine quality label based on format information"""
self = cast("YTSageApp", self) # for autocompletion and type inference.
if format_info.get("vcodec") == "none": if format_info.get("vcodec") == "none":
# Audio quality # Audio quality
abr = format_info.get("abr", 0) abr = format_info.get("abr", 0)
if abr >= 256: if abr >= 256:
return "Best Audio" return _("formats.best_audio")
elif abr >= 192: elif abr >= 192:
return "High Audio" return _("formats.high_audio")
elif abr >= 128: elif abr >= 128:
return "Medium Audio" return _("formats.medium_audio")
else: else:
return "Low Audio" return _("formats.low_audio")
else: else:
# Video quality # Video quality
height = 0 height = 0
@@ -369,55 +462,16 @@ class FormatTableMixin:
pass pass
if height >= 2160: if height >= 2160:
return "Best (4K)" return _("formats.best_4k")
elif height >= 1440: elif height >= 1440:
return "Best (2K)" return _("formats.best_2k")
elif height >= 1080: elif height >= 1080:
return "High (1080p)" return _("formats.high_1080p")
elif height >= 720: elif height >= 720:
return "High (720p)" return _("formats.high_720p")
elif height >= 480: elif height >= 480:
return "Medium (480p)" return _("formats.medium_480p")
else: else:
return "Low Quality" return _("formats.low_quality")
def _get_format_notes(self, format_info) -> str:
"""Generate helpful format notes based on format info."""
notes = []
# Add storage indicator with more granular categories
file_size = format_info.get("filesize") or format_info.get("filesize_approx", 0)
resolution = format_info.get("resolution", "")
height = 0
if resolution:
try:
height = int(resolution.split("x")[1])
except:
pass
# Better file size categories
if file_size > 50 * 1024 * 1024: # Over 50MB
notes.append("Large size")
elif file_size > 15 * 1024 * 1024: # 15-50MB
notes.append("Medium size")
elif file_size > 5 * 1024 * 1024: # 5-15MB
notes.append("Standard size")
else: # Under 5MB
notes.append("Small size")
# Add codec quality indicator
vcodec = format_info.get("vcodec", "")
if vcodec != "none":
if "avc1" in vcodec: # H.264
notes.append("Compatible")
elif "av01" in vcodec: # AV1
notes.append("Efficient")
elif "vp9" in vcodec: # VP9
notes.append("High quality")
# Add quick mobile compatibility check
if "avc1" in vcodec and file_size < 8 * 1024 * 1024:
notes.append("Mobile")
# Return simple string
return "".join(notes)
+371 -229
View File
File diff suppressed because it is too large Load Diff
+51 -36
View File
@@ -2,22 +2,29 @@ import re
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, cast
import requests import requests
from PIL import Image from PIL import Image
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from src.core.ytsage_logging import logger
from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py
SponsorBlockCategoryDialog, SponsorBlockCategoryDialog,
SubtitleSelectionDialog, SubtitleSelectionDialog,
) )
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp
class VideoInfoMixin: class VideoInfoMixin:
def setup_video_info_section(self) -> QHBoxLayout: 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 # Create a horizontal layout for thumbnail and video info
media_info_layout = QHBoxLayout() media_info_layout = QHBoxLayout()
media_info_layout.setSpacing(15) media_info_layout.setSpacing(15)
@@ -89,7 +96,7 @@ class VideoInfoMixin:
subtitle_layout.setSpacing(10) subtitle_layout.setSpacing(10)
# Subtitle selection button # Subtitle selection button
self.subtitle_select_btn = QPushButton("Select Subtitles...") # Renamed & changed text self.subtitle_select_btn = QPushButton(_("main_ui.select_subtitles")) # Renamed & changed text
self.subtitle_select_btn.setFixedHeight(30) self.subtitle_select_btn.setFixedHeight(30)
# self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed # 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.clicked.connect(self.open_subtitle_dialog)
@@ -118,7 +125,7 @@ class VideoInfoMixin:
subtitle_layout.addWidget(self.subtitle_select_btn) subtitle_layout.addWidget(self.subtitle_select_btn)
# Label to show number of selected subtitles # Label to show number of selected subtitles
self.selected_subs_label = QLabel("0 selected") self.selected_subs_label = QLabel(_("selection.none_selected"))
self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;") self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
subtitle_layout.addWidget(self.selected_subs_label) subtitle_layout.addWidget(self.selected_subs_label)
@@ -132,7 +139,7 @@ class VideoInfoMixin:
# --- SponsorBlock Section --- # --- SponsorBlock Section ---
sponsorblock_layout = QHBoxLayout() sponsorblock_layout = QHBoxLayout()
self.sponsorblock_select_btn = QPushButton("SponsorBlock Categories...") self.sponsorblock_select_btn = QPushButton(_("main_ui.sponsorblock_categories"))
self.sponsorblock_select_btn.setFixedHeight(30) self.sponsorblock_select_btn.setFixedHeight(30)
self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog) self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog)
self.sponsorblock_select_btn.setStyleSheet( self.sponsorblock_select_btn.setStyleSheet(
@@ -161,7 +168,7 @@ class VideoInfoMixin:
sponsorblock_layout.addWidget(self.sponsorblock_select_btn) sponsorblock_layout.addWidget(self.sponsorblock_select_btn)
# Label to show selection count # Label to show selection count
self.selected_sponsorblock_label = QLabel("0 selected") self.selected_sponsorblock_label = QLabel(_("selection.none_selected"))
self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;") self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
sponsorblock_layout.addWidget(self.selected_sponsorblock_label) sponsorblock_layout.addWidget(self.selected_sponsorblock_label)
@@ -184,6 +191,8 @@ class VideoInfoMixin:
return media_info_layout return media_info_layout
def setup_playlist_info_section(self) -> QLabel: 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 = QLabel()
self.playlist_info_label.setVisible(False) self.playlist_info_label.setVisible(False)
self.playlist_info_label.setStyleSheet( self.playlist_info_label.setStyleSheet(
@@ -205,12 +214,14 @@ class VideoInfoMixin:
return self.playlist_info_label return self.playlist_info_label
def update_video_info(self, info) -> None: 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: if hasattr(self, "is_playlist") and self.is_playlist:
# Playlist Mode: Show playlist title and video count # Playlist Mode: Show playlist title and video count
self.title_label.setText(self.playlist_info.get("title", "Unknown Playlist")) self.title_label.setText(self.playlist_info.get("title", _("playlist.unknown")))
num_videos = len(getattr(self, "playlist_entries", [])) num_videos = len(getattr(self, "playlist_entries", []))
self.duration_label.setText(f"Total Videos: {num_videos}") self.duration_label.setText(_("playlist.total_videos", count=num_videos))
# Hide video-specific info # Hide video-specific info
self.channel_label.setText("") self.channel_label.setText("")
@@ -243,7 +254,7 @@ class VideoInfoMixin:
date_obj = datetime.strptime(upload_date, "%Y%m%d") date_obj = datetime.strptime(upload_date, "%Y%m%d")
formatted_date = date_obj.strftime("%B %d, %Y") formatted_date = date_obj.strftime("%B %d, %Y")
else: else:
formatted_date = "Unknown date" formatted_date = _("video_info.unknown_date")
# Format duration # Format duration
duration = info.get("duration", 0) duration = info.get("duration", 0)
@@ -251,15 +262,17 @@ class VideoInfoMixin:
seconds = duration % 60 seconds = duration % 60
duration_str = f"{minutes}:{seconds:02d}" duration_str = f"{minutes}:{seconds:02d}"
# Update labels # Update labels with localized text
self.title_label.setText(info.get("title", "Unknown title")) self.title_label.setText(info.get("title", _("video_info.unknown_title")))
self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}") self.channel_label.setText(f"{_("video_info.channel")}: {info.get('uploader', _("video_info.unknown_channel"))}")
self.views_label.setText(f"Views: {formatted_views}") self.views_label.setText(f"{_("video_info.views")}: {formatted_views}")
self.like_count_label.setText(f"Likes: {formatted_likes}") self.like_count_label.setText(f"{_("video_info.likes")}: {formatted_likes}")
self.date_label.setText(f"Upload date: {formatted_date}") self.date_label.setText(f"{_("video_info.upload_date")}: {formatted_date}")
self.duration_label.setText(f"Duration: {duration_str}") self.duration_label.setText(f"{_("video_info.duration")}: {duration_str}")
def open_subtitle_dialog(self) -> None: 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"): if not hasattr(self, "available_subtitles") or not hasattr(self, "available_automatic_subtitles"):
logger.warning("Subtitle info not loaded yet.") logger.warning("Subtitle info not loaded yet.")
return return
@@ -274,29 +287,21 @@ class VideoInfoMixin:
self, # Parent for the dialog self, # Parent for the dialog
) )
# Access the main application window (parent of the mixin's widget) # removed extra logic for mapping to main_windows
# to find the merge checkbox merge_checkbox = getattr(self, "merge_subs_checkbox", None)
main_window = self # In this context, self should be the YTSageApp instance
if not isinstance(main_window, QMainWindow):
# If the structure is different, this might need adjustment
# Maybe self.parentWidget() or similar depending on how Mixin is used
logger.warning("Cannot find main window to access merge checkbox.")
merge_checkbox = None
else:
merge_checkbox = getattr(main_window, "merge_subs_checkbox", None)
if dialog.exec(): # If user clicks OK if dialog.exec(): # If user clicks OK
self.selected_subtitles = dialog.get_selected_subtitles() self.selected_subtitles = dialog.get_selected_subtitles()
logger.info(f"Selected subtitles: {self.selected_subtitles}") logger.info(f"Selected subtitles: {self.selected_subtitles}")
# Update UI to reflect selection # Update UI to reflect selection
count = len(self.selected_subtitles) count = len(self.selected_subtitles)
self.selected_subs_label.setText(f"{count} selected") self.selected_subs_label.setText(_("subtitle_selection.count_selected", count=count))
self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0) self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0)
# Enable/disable the merge checkbox in the parent window # Enable/disable the merge checkbox in the parent window
if merge_checkbox: if merge_checkbox:
# Only enable merge checkbox if we're not in Audio Only mode # Only enable merge checkbox if we're not in Audio Only mode
is_audio_only = hasattr(main_window, "audio_button") and main_window.audio_button.isChecked() is_audio_only = hasattr(self, "audio_button") and self.audio_button.isChecked()
# In audio-only mode, we still allow subtitle selection but not merging # In audio-only mode, we still allow subtitle selection but not merging
should_enable = count > 0 and not is_audio_only should_enable = count > 0 and not is_audio_only
merge_checkbox.setEnabled(should_enable) merge_checkbox.setEnabled(should_enable)
@@ -310,6 +315,8 @@ class VideoInfoMixin:
def open_sponsorblock_dialog(self) -> None: def open_sponsorblock_dialog(self) -> None:
"""Open the SponsorBlock category selection dialog.""" """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) # Initialize selected categories if not exists or empty (first time opening)
if not hasattr(self, "selected_sponsorblock_categories") or not self.selected_sponsorblock_categories: if not hasattr(self, "selected_sponsorblock_categories") or not self.selected_sponsorblock_categories:
# Use None to let the dialog set its own defaults # Use None to let the dialog set its own defaults
@@ -326,6 +333,8 @@ class VideoInfoMixin:
def _update_sponsorblock_display(self) -> None: def _update_sponsorblock_display(self) -> None:
"""Update the SponsorBlock button and label to reflect current selection.""" """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"): if not hasattr(self, "selected_sponsorblock_categories"):
self.selected_sponsorblock_categories = [] self.selected_sponsorblock_categories = []
@@ -333,11 +342,11 @@ class VideoInfoMixin:
# Update label text # Update label text
if count == 0: if count == 0:
self.selected_sponsorblock_label.setText("0 selected") self.selected_sponsorblock_label.setText(_("selection.none_selected"))
elif count == 1: elif count == 1:
self.selected_sponsorblock_label.setText("1 category selected") self.selected_sponsorblock_label.setText(_("selection.one_selected"))
else: else:
self.selected_sponsorblock_label.setText(f"{count} categories selected") self.selected_sponsorblock_label.setText(_("selection.count_selected", count=count))
# Update button property for styling # Update button property for styling
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0) self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0)
@@ -347,6 +356,8 @@ class VideoInfoMixin:
self.sponsorblock_select_btn.style().polish(self.sponsorblock_select_btn) self.sponsorblock_select_btn.style().polish(self.sponsorblock_select_btn)
def download_thumbnail(self, url) -> None: def download_thumbnail(self, url) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference.
try: try:
# Store both thumbnail URL and video URL # Store both thumbnail URL and video URL
self.thumbnail_url = url self.thumbnail_url = url
@@ -364,7 +375,10 @@ class VideoInfoMixin:
pixmap.loadFromData(img_byte_arr.getvalue()) pixmap.loadFromData(img_byte_arr.getvalue())
self.thumbnail_label.setPixmap(pixmap) self.thumbnail_label.setPixmap(pixmap)
except Exception as e: except Exception as e:
logger.error(f"Error loading thumbnail: {str(e)}") logger.exception(f"Error loading thumbnail: {e}")
def download_thumbnail_file(self, video_url, path) -> bool:
self = cast("YTSageApp", self) # for autocompletion and type inference.
def download_thumbnail_file(self, video_url, path) -> bool: def download_thumbnail_file(self, video_url, path) -> bool:
if not self.save_thumbnail: if not self.save_thumbnail:
@@ -377,6 +391,7 @@ class VideoInfoMixin:
logger.debug(f"Attempting to save thumbnail for URL: {video_url}") logger.debug(f"Attempting to save thumbnail for URL: {video_url}")
ydl_opts = { ydl_opts = {
"logger": logger,
"quiet": True, "quiet": True,
"skip_download": True, "skip_download": True,
"force_generic_extractor": False, "force_generic_extractor": False,
@@ -389,7 +404,7 @@ class VideoInfoMixin:
thumbnails = info.get("thumbnails", []) thumbnails = info.get("thumbnails", [])
if not thumbnails: if not thumbnails:
raise ValueError("No thumbnails available") logger.info("No thumbnails available")
thumbnail_url = max( thumbnail_url = max(
thumbnails, thumbnails,
@@ -397,7 +412,7 @@ class VideoInfoMixin:
).get("url") ).get("url")
if not thumbnail_url: if not thumbnail_url:
raise ValueError("Failed to extract thumbnail URL") logger.info("Failed to extract thumbnail URL")
# Download using requests # Download using requests
response = requests.get(thumbnail_url) response = requests.get(thumbnail_url)
@@ -418,8 +433,8 @@ class VideoInfoMixin:
return True return True
except Exception as e: except Exception as e:
error_msg = f"❌ Thumbnail error: {str(e)}" error_msg = f"❌ Thumbnail error: {e}"
logger.error(f"Thumbnail Save Error: {str(e)}") logger.exception(f"Thumbnail Save Error: {e}")
self.signals.update_status.emit(error_msg) self.signals.update_status.emit(error_msg)
return False return False
+205
View File
@@ -0,0 +1,205 @@
"""
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 src.utils.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 typing import Any
from src.utils.ytsage_constants import APP_CONFIG_FILE, USER_HOME_DIR
from src.utils.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()
_config_file = APP_CONFIG_FILE
_settings: dict[str, Any] = {}
_default_config = {
"download_path": str(USER_HOME_DIR / "Downloads"),
"speed_limit_value": None,
"speed_limit_unit_index": 0,
"cookie_file_path": None,
"last_used_cookie_file": None,
"proxy_url": None,
"geo_proxy_url": None,
"auto_update_ytdlp": True,
"auto_update_frequency": "daily",
"last_update_check": 0,
"language": "en",
"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) -> 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").
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 = key.split(".")
value = 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 = key.split(".")
d = 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 = key.split(".")
d = 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.")
+33 -11
View File
@@ -30,6 +30,29 @@ def get_asset_path(asset_relative_path: str) -> Path:
Returns: Returns:
Path: Absolute path to the asset file 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: try:
# Use importlib.resources (standard in Python 3.9+) # Use importlib.resources (standard in Python 3.9+)
import importlib.resources as resources import importlib.resources as resources
@@ -59,13 +82,15 @@ SOUND_PATH: Path = get_asset_path("assets/sound/notification.mp3")
OS_NAME: str = platform.system() # Windows ; Darwin ; Linux OS_NAME: str = platform.system() # Windows ; Darwin ; Linux
IS_FROZEN = getattr(sys, "frozen", False)
USER_HOME_DIR: Path = Path.home() USER_HOME_DIR: Path = Path.home()
# OS Specific Constants # OS Specific Constants
if OS_NAME == "Windows": if OS_NAME == "Windows":
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}" OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
# APP_PATH will be from system environment path or fallback to Path.home() # 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_DIR: Path = Path(os.environ.get("LOCALAPPDATA", USER_HOME_DIR / "AppData" / "Local")) / "YTSage"
APP_BIN_DIR: Path = APP_DIR / "bin" APP_BIN_DIR: Path = APP_DIR / "bin"
APP_DATA_DIR: Path = APP_DIR / "data" APP_DATA_DIR: Path = APP_DIR / "data"
@@ -75,15 +100,14 @@ if OS_NAME == "Windows":
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe" 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" YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp.exe"
# Documentation URLs
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
SUBPROCESS_CREATIONFLAGS: int = subprocess.CREATE_NO_WINDOW SUBPROCESS_CREATIONFLAGS: int = subprocess.CREATE_NO_WINDOW
elif OS_NAME == "Darwin": # macOS elif OS_NAME == "Darwin": # macOS
_mac_version = platform.mac_ver()[0] _mac_version = platform.mac_ver()[0]
OS_FULL_NAME: str = f"macOS {_mac_version}" if _mac_version else "macOS" 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_DIR: Path = USER_HOME_DIR / "Library" / "Application Support" / "YTSage"
APP_BIN_DIR: Path = APP_DIR / "bin" APP_BIN_DIR: Path = APP_DIR / "bin"
APP_DATA_DIR: Path = APP_DIR / "data" APP_DATA_DIR: Path = APP_DIR / "data"
@@ -93,15 +117,14 @@ elif OS_NAME == "Darwin": # macOS
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos" 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" YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp"
# Documentation URLs
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
SUBPROCESS_CREATIONFLAGS: int = 0 SUBPROCESS_CREATIONFLAGS: int = 0
else: # Linux and other UNIX-like else: # Linux and other UNIX-like
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}" 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_DIR: Path = USER_HOME_DIR / ".local" / "share" / "YTSage"
APP_BIN_DIR: Path = APP_DIR / "bin" APP_BIN_DIR: Path = APP_DIR / "bin"
APP_DATA_DIR: Path = APP_DIR / "data" APP_DATA_DIR: Path = APP_DIR / "data"
@@ -111,11 +134,10 @@ else: # Linux and other UNIX-like
YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" YTDLP_DOWNLOAD_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp" YTDLP_APP_BIN_PATH: Path = APP_BIN_DIR / "yt-dlp"
# Documentation URLs
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
SUBPROCESS_CREATIONFLAGS: int = 0 SUBPROCESS_CREATIONFLAGS: int = 0
# Documentation URLs
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
# ffmpeg download links # ffmpeg download links
FFMPEG_7Z_DOWNLOAD_URL = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.7z" FFMPEG_7Z_DOWNLOAD_URL = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.7z"
@@ -124,6 +146,7 @@ FFMPEG_ZIP_DOWNLOAD_URL = "https://github.com/GyanD/codexffmpeg/releases/downloa
if __name__ == "__main__": if __name__ == "__main__":
# If this file is run directly, print directory information; if imported, create the necessary directories for the application. # If this file is run directly, print directory information; if imported, create the necessary directories for the application.
# for debug, to check os specific variable which can be different based on os.
info = { info = {
"OS_NAME": OS_NAME, "OS_NAME": OS_NAME,
"OS_FULL_NAME": OS_FULL_NAME, "OS_FULL_NAME": OS_FULL_NAME,
@@ -135,7 +158,6 @@ if __name__ == "__main__":
"APP_CONFIG_FILE": str(APP_CONFIG_FILE), "APP_CONFIG_FILE": str(APP_CONFIG_FILE),
"YTDLP_DOWNLOAD_URL": YTDLP_DOWNLOAD_URL, "YTDLP_DOWNLOAD_URL": YTDLP_DOWNLOAD_URL,
"YTDLP_APP_BIN_PATH": YTDLP_APP_BIN_PATH, "YTDLP_APP_BIN_PATH": YTDLP_APP_BIN_PATH,
"YTDLP_DOCS_URL": YTDLP_DOCS_URL,
"SUBPROCESS_CREATIONFLAGS": SUBPROCESS_CREATIONFLAGS, "SUBPROCESS_CREATIONFLAGS": SUBPROCESS_CREATIONFLAGS,
} }
for key, value in info.items(): for key, value in info.items():
+278
View File
@@ -0,0 +1,278 @@
"""
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 src.utils.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 src.utils.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.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"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"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:"
}
}
@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 src.utils.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"]