Merge branch 'beta'
@@ -5,7 +5,7 @@ This repository uses GitHub Actions to automatically build and release YTSage fo
|
||||
## How It Works
|
||||
|
||||
### 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
|
||||
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**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Release v4.8.0"
|
||||
git commit -m "Release v4.9.0"
|
||||
```
|
||||
|
||||
3. **Create and push a tag**:
|
||||
```bash
|
||||
git tag v4.8.0
|
||||
git push origin v4.8.0
|
||||
git tag v4.9.0
|
||||
git push origin v4.9.0
|
||||
```
|
||||
|
||||
4. **Watch the action**: Go to Actions tab in GitHub to monitor progress
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: '3.13.6'
|
||||
PYTHON_VERSION: '3.13'
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
echo "VERSION=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
venv
|
||||
@@ -122,7 +122,10 @@ jobs:
|
||||
],
|
||||
include_files=[
|
||||
("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"),
|
||||
("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"),
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: '3.13.6'
|
||||
PYTHON_VERSION: '3.13'
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -34,12 +34,12 @@ jobs:
|
||||
echo "VERSION=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
venv
|
||||
@@ -123,7 +123,10 @@ jobs:
|
||||
],
|
||||
include_files=[
|
||||
("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"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: '3.13.6'
|
||||
PYTHON_VERSION: '3.13'
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
venv
|
||||
@@ -114,7 +114,10 @@ jobs:
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value " ],"
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value " include_files=["
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("src", "src"),'
|
||||
Add-Content -Path "setup_cxfreeze.py" -Value ' ("assets", "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 ""
|
||||
|
||||
@@ -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 |
|
||||
| 📋 Playlist Support | 🖼️ Save thumbnail | ⚙️ Custom Commands |
|
||||
| 🖼️ Playlist Selector | 🚀 Speed Limiter | 🍪 Login with Cookies |
|
||||
| 📑 Embed Chapters | ✂️ Trim Video Sections | |
|
||||
| 📑 Embed Chapters | ✂️ Trim Video Sections | 🌐 Proxy Support |
|
||||
|
||||
</div>
|
||||
|
||||
@@ -214,6 +214,28 @@ python main.py
|
||||
- **Update yt-dlp:** Update 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)
|
||||
- **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>
|
||||
|
||||
@@ -330,6 +352,21 @@ YTSage/
|
||||
│ │ └── icon.png
|
||||
│ └── 📁 sound/ # Audio files
|
||||
│ └── notification.mp3
|
||||
├── 📁 languages/ # Localization files
|
||||
│ ├── 📄 ar.json # Arabic translation
|
||||
│ ├── 📄 de.json # German translation
|
||||
│ ├── 📄 en.json # English translation
|
||||
│ ├── 📄 es.json # Spanish translation
|
||||
│ ├── 📄 fr.json # French translation
|
||||
│ ├── 📄 hi.json # Hindi translation
|
||||
│ ├── 📄 id.json # Indonesian translation
|
||||
│ ├── 📄 it.json # Italian translation
|
||||
│ ├── 📄 ja.json # Japanese translation
|
||||
│ ├── 📄 pl.json # Polish translation
|
||||
│ ├── 📄 pt.json # Portuguese translation
|
||||
│ ├── 📄 ru.json # Russian translation
|
||||
│ ├── 📄 tr.json # Turkish translation
|
||||
│ └── 📄 zh.json # Chinese translation
|
||||
├── 📄 LICENSE # License file
|
||||
├── 📄 main.py # Application entry point
|
||||
├── 📄 README.md # Project documentation
|
||||
@@ -340,7 +377,6 @@ YTSage/
|
||||
│ ├── 📄 __init__.py # Core package init
|
||||
│ ├── 📄 ytsage_downloader.py # Download functionality
|
||||
│ ├── 📄 ytsage_ffmpeg.py # FFmpeg integration
|
||||
│ ├── 📄 ytsage_logging.py # Logging utilities
|
||||
│ ├── 📄 ytsage_style.py # UI styling
|
||||
│ ├── 📄 ytsage_utils.py # Utility functions
|
||||
│ └── 📄 ytsage_yt_dlp.py # yt-dlp integration
|
||||
@@ -359,7 +395,10 @@ YTSage/
|
||||
│ └── 📄 ytsage_dialogs_update.py # Update dialogs
|
||||
└── 📁 utils/ # Utility modules
|
||||
├── 📄 __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>
|
||||
|
||||
|
Before Width: | Height: | Size: 695 KiB After Width: | Height: | Size: 798 KiB |
|
Before Width: | Height: | Size: 648 KiB After Width: | Height: | Size: 769 KiB |
|
Before Width: | Height: | Size: 593 KiB After Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 676 KiB After Width: | Height: | Size: 815 KiB |
@@ -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": "🔍 التحقق من التحديثات الآن"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "🔍 अभी अपडेट की जांच करें"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "🔍 今すぐ更新を確認"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "🔍 Проверить обновления сейчас"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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• 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": "🐍 正在通过 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,11 +2,8 @@ import sys
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_yt_dlp import ( # Import the new yt-dlp setup functions
|
||||
check_ytdlp_binary,
|
||||
setup_ytdlp,
|
||||
)
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.core.ytsage_yt_dlp import check_ytdlp_binary, setup_ytdlp # Import the new yt-dlp setup functions
|
||||
from src.gui.ytsage_gui_main import YTSageApp # Import the main application class from ytsage_gui_main
|
||||
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader
|
||||
A modern, user-friendly YouTube video downloader built with PySide6.
|
||||
"""
|
||||
|
||||
__version__ = "4.8.3"
|
||||
__version__ = "4.9.0b"
|
||||
__author__ = "oop7"
|
||||
|
||||
@@ -7,9 +7,13 @@ from pathlib import Path
|
||||
|
||||
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.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:
|
||||
import yt_dlp # Keep yt_dlp import here - only downloader uses it.
|
||||
@@ -38,6 +42,7 @@ class DownloadThread(QThread):
|
||||
error_signal = Signal(str)
|
||||
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
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -58,6 +63,8 @@ class DownloadThread(QThread):
|
||||
rate_limit=None,
|
||||
download_section=None,
|
||||
force_keyframes=False,
|
||||
proxy_url=None,
|
||||
geo_proxy_url=None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.url = url
|
||||
@@ -77,6 +84,8 @@ class DownloadThread(QThread):
|
||||
self.rate_limit = rate_limit
|
||||
self.download_section = download_section
|
||||
self.force_keyframes = force_keyframes
|
||||
self.proxy_url = proxy_url
|
||||
self.geo_proxy_url = geo_proxy_url
|
||||
self.paused = False
|
||||
self.cancelled = False
|
||||
self.process = None
|
||||
@@ -94,12 +103,31 @@ class DownloadThread(QThread):
|
||||
pattern = re.compile(r"\.f\d+\.") # Pattern to match format codes like .f243.
|
||||
for file_path in self.path.iterdir():
|
||||
if file_path.suffix == ".part" or pattern.search(file_path.name):
|
||||
try:
|
||||
file_path.unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting {file_path.name}: {str(e)}")
|
||||
self._safe_delete_with_retry(file_path)
|
||||
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:
|
||||
"""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}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting subtitle file {path}: {e}")
|
||||
logger.exception(f"Error deleting subtitle file {path}: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -130,7 +158,7 @@ class DownloadThread(QThread):
|
||||
else:
|
||||
logger.debug(f"Deleted {deleted_count[1]} of {len(new_subtitle_files)} new subtitle files")
|
||||
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:
|
||||
"""Check if the file already exists before downloading"""
|
||||
@@ -138,18 +166,28 @@ class DownloadThread(QThread):
|
||||
logger.debug("Starting file existence check")
|
||||
# Use yt-dlp to get the filename without downloading, suppressing warnings
|
||||
ydl_opts_check = {
|
||||
"logger": logger, # passed app logger
|
||||
"quiet": True,
|
||||
"skip_download": True,
|
||||
"no_warnings": True, # <-- Suppress warnings during 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
|
||||
}
|
||||
if self.cookie_file:
|
||||
ydl_opts_check["cookiefile"] = str(self.cookie_file)
|
||||
elif self.browser_cookies:
|
||||
ydl_opts_check["cookiesfrombrowser"] = (self.browser_cookies.split(':')[0],
|
||||
self.browser_cookies.split(':')[1] if ':' in self.browser_cookies else None)
|
||||
ydl_opts_check["cookiesfrombrowser"] = (
|
||||
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:
|
||||
with yt_dlp.YoutubeDL(ydl_opts_check) as ydl:
|
||||
@@ -178,10 +216,7 @@ class DownloadThread(QThread):
|
||||
return False # Proceed with download attempt
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking file existence: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.exception(f"Error checking file existence: {e}")
|
||||
return None
|
||||
|
||||
def _build_yt_dlp_command(self) -> list:
|
||||
@@ -201,6 +236,7 @@ class DownloadThread(QThread):
|
||||
try:
|
||||
if YT_DLP_AVAILABLE:
|
||||
ydl_opts = {
|
||||
"logger": logger,
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
@@ -214,7 +250,7 @@ class DownloadThread(QThread):
|
||||
logger.debug(f"Detected audio-only format for ID: {clean_format_id}")
|
||||
break
|
||||
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
|
||||
if is_audio_format:
|
||||
@@ -229,12 +265,9 @@ class DownloadThread(QThread):
|
||||
try:
|
||||
format_ext = None
|
||||
logger.debug(f"Getting format information for format ID: {self.format_id} (using: {clean_format_id})")
|
||||
|
||||
if YT_DLP_AVAILABLE:
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
}
|
||||
ydl_opts = {"quiet": True, "no_warnings": True, "skip_download": True, "logger": logger}
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(self.url, download=False) or {}
|
||||
# Look for the clean format ID first
|
||||
@@ -254,7 +287,7 @@ class DownloadThread(QThread):
|
||||
# Ensure output matches the selected format - only for video formats
|
||||
cmd.extend(["--merge-output-format", format_ext])
|
||||
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
|
||||
pass
|
||||
else:
|
||||
@@ -272,7 +305,7 @@ class DownloadThread(QThread):
|
||||
else:
|
||||
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
|
||||
cmd.append("--force-overwrites")
|
||||
@@ -295,7 +328,7 @@ class DownloadThread(QThread):
|
||||
lang_code = sub_selection.split(" - ")[0]
|
||||
lang_codes.append(lang_code)
|
||||
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:
|
||||
cmd.extend(["--sub-langs", ",".join(lang_codes)])
|
||||
@@ -324,6 +357,13 @@ class DownloadThread(QThread):
|
||||
elif self.browser_cookies:
|
||||
cmd.extend(["--cookies-from-browser", self.browser_cookies])
|
||||
|
||||
# Add proxy settings if specified
|
||||
if self.proxy_url:
|
||||
cmd.extend(["--proxy", self.proxy_url])
|
||||
|
||||
if self.geo_proxy_url:
|
||||
cmd.extend(["--geo-verification-proxy", self.geo_proxy_url])
|
||||
|
||||
# Add rate limit if specified
|
||||
if self.rate_limit:
|
||||
cmd.extend(["-r", self.rate_limit])
|
||||
@@ -366,7 +406,7 @@ class DownloadThread(QThread):
|
||||
self.initial_subtitle_files.add(file)
|
||||
logger.debug(f"Found {len(self.initial_subtitle_files)} existing subtitle files before download")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error scanning for initial subtitle files: {e}")
|
||||
logger.exception(f"Error scanning for initial subtitle files: {e}")
|
||||
|
||||
if self.use_direct_command:
|
||||
# Use direct CLI command instead of Python API
|
||||
@@ -377,10 +417,8 @@ class DownloadThread(QThread):
|
||||
|
||||
except Exception as e:
|
||||
# Catch errors during setup
|
||||
self.error_signal.emit(f"Critical error in download thread: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.critical(f"Critical error in download thread: {e}", exc_info=True)
|
||||
self.error_signal.emit(f"Critical error in download thread: {e}")
|
||||
|
||||
def _run_direct_command(self) -> None:
|
||||
"""Run yt-dlp as a direct command line process instead of using Python API."""
|
||||
@@ -389,7 +427,7 @@ class DownloadThread(QThread):
|
||||
cmd_str = " ".join(shlex.quote(str(arg)) for arg in cmd)
|
||||
logger.debug(f"Executing command: {cmd_str}")
|
||||
|
||||
self.status_signal.emit("🚀 Starting download...")
|
||||
self.status_signal.emit(_("download.starting"))
|
||||
self.progress_signal.emit(0)
|
||||
|
||||
# Start the process
|
||||
@@ -409,8 +447,18 @@ class DownloadThread(QThread):
|
||||
for line in iter(self.process.stdout.readline, ""): # type: ignore
|
||||
if self.cancelled:
|
||||
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.status_signal.emit("Download cancelled")
|
||||
self.status_signal.emit(_("download.cancelled"))
|
||||
return
|
||||
|
||||
# Wait if paused
|
||||
@@ -427,20 +475,20 @@ class DownloadThread(QThread):
|
||||
# return code 127 typically means command not found
|
||||
if return_code == 127:
|
||||
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
|
||||
|
||||
if return_code == 0:
|
||||
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
|
||||
# to ensure the embedding process has completed
|
||||
if self.merge_subs:
|
||||
# Add a significant delay to ensure ffmpeg has released all file handles
|
||||
# and any post-processing is complete
|
||||
self.status_signal.emit("✅ Download completed! Cleaning up...")
|
||||
self.status_signal.emit(_("download.completed_cleaning"))
|
||||
time.sleep(3) # Increased delay to 3 seconds
|
||||
self.cleanup_subtitle_files()
|
||||
|
||||
@@ -448,7 +496,7 @@ class DownloadThread(QThread):
|
||||
else:
|
||||
# Check if it was cancelled
|
||||
if self.cancelled:
|
||||
self.status_signal.emit("Download cancelled")
|
||||
self.status_signal.emit(_("download.cancelled"))
|
||||
else:
|
||||
# Provide more descriptive error message for possible yt-dlp conflicts
|
||||
if return_code == 1:
|
||||
@@ -457,10 +505,16 @@ class DownloadThread(QThread):
|
||||
)
|
||||
else:
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
def _parse_output_line(self, line) -> None:
|
||||
@@ -499,31 +553,31 @@ class DownloadThread(QThread):
|
||||
|
||||
# Check if this is explicitly an audio stream download
|
||||
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
|
||||
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
|
||||
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
|
||||
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
|
||||
self.status_signal.emit(f"⏬ Downloading subtitle...")
|
||||
self.status_signal.emit(_("download.downloading_subtitle"))
|
||||
# Default case
|
||||
else:
|
||||
self.status_signal.emit(f"⏬ Downloading...")
|
||||
self.status_signal.emit(_("download.downloading"))
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting filename from line '{line}': {e}")
|
||||
self.status_signal.emit("⚡ Downloading...") # Fallback status
|
||||
logger.exception(f"Error extracting filename from line '{line}': {e}")
|
||||
self.status_signal.emit(_("download.downloading_fallback")) # Fallback status
|
||||
return # Don't process this line further for speed/ETA
|
||||
|
||||
# Check for specific download types in the output
|
||||
if "Downloading video" in line:
|
||||
self.status_signal.emit(f"⏬ Downloading video...")
|
||||
self.status_signal.emit(_("download.downloading_video"))
|
||||
return
|
||||
|
||||
elif "Downloading audio" in line:
|
||||
self.status_signal.emit(f"⏬ Downloading audio...")
|
||||
self.status_signal.emit(_("download.downloading_audio"))
|
||||
return
|
||||
|
||||
# Detect subtitle file creation
|
||||
@@ -535,18 +589,18 @@ class DownloadThread(QThread):
|
||||
)
|
||||
if subtitle_match:
|
||||
subtitle_file = subtitle_match.group(1).strip()
|
||||
|
||||
|
||||
# Clean up the path - remove any duplicated directory paths
|
||||
# Sometimes yt-dlp output contains malformed paths like "dir: dir/file"
|
||||
if ":" in subtitle_file and os.name == 'nt': # Windows paths
|
||||
if ":" in subtitle_file and os.name == "nt": # Windows paths
|
||||
# Look for pattern like "C:\path: C:\path\file" and extract the latter
|
||||
colon_parts = subtitle_file.split(": ")
|
||||
if len(colon_parts) > 1:
|
||||
# Take the last part which should be the actual file path
|
||||
subtitle_file = colon_parts[-1].strip()
|
||||
|
||||
|
||||
# Show subtitle download message
|
||||
self.status_signal.emit(f"⏬ Downloading subtitle...")
|
||||
self.status_signal.emit(_("download.downloading_subtitle"))
|
||||
# Store the subtitle file path for later deletion if merging is enabled
|
||||
if self.merge_subs:
|
||||
subtitle_path = Path(subtitle_file)
|
||||
@@ -559,24 +613,24 @@ class DownloadThread(QThread):
|
||||
|
||||
# Send status updates based on output line content
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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:
|
||||
self.status_signal.emit("⏬ Downloading audio...")
|
||||
self.status_signal.emit(_("download.downloading_audio"))
|
||||
elif "Downloading format" in line:
|
||||
# Try to detect if it's audio or video format
|
||||
if " - audio only" in line:
|
||||
self.status_signal.emit("⏬ Downloading audio...")
|
||||
self.status_signal.emit(_("download.downloading_audio"))
|
||||
elif " - video only" in line:
|
||||
self.status_signal.emit("⏬ Downloading video...")
|
||||
self.status_signal.emit(_("download.downloading_video"))
|
||||
else:
|
||||
# Don't emit generic message - format is unclear
|
||||
pass
|
||||
@@ -603,19 +657,19 @@ class DownloadThread(QThread):
|
||||
eta_str = eta_match.group(1) if eta_match else "N/A"
|
||||
|
||||
# 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)
|
||||
except Exception as e:
|
||||
# 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
|
||||
|
||||
# Check for post-processing
|
||||
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)
|
||||
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)
|
||||
elif "Deleting original file" in line:
|
||||
self.progress_signal.emit(98)
|
||||
@@ -639,7 +693,7 @@ class DownloadThread(QThread):
|
||||
self.file_exists_signal.emit(filename)
|
||||
else:
|
||||
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:
|
||||
self.progress_signal.emit(100)
|
||||
|
||||
@@ -649,18 +703,18 @@ class DownloadThread(QThread):
|
||||
|
||||
# Video file extensions
|
||||
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
|
||||
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
|
||||
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
|
||||
self.status_signal.emit(f"✅ Subtitle download completed!")
|
||||
self.status_signal.emit(_("download.subtitle_completed"))
|
||||
# Default case
|
||||
else:
|
||||
self.status_signal.emit("✅ Download completed!")
|
||||
self.status_signal.emit(_("download.completed"))
|
||||
else:
|
||||
self.status_signal.emit("✅ Download completed!")
|
||||
self.status_signal.emit(_("download.completed"))
|
||||
|
||||
self.update_details.emit("") # Clear details label on completion
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
FFMPEG_7Z_DOWNLOAD_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}%")
|
||||
return True
|
||||
except requests.RequestException as e:
|
||||
logger.info(f"Download error: {str(e)}")
|
||||
logger.info(f"Download error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ def verify_sha256(file_path, expected_hash_url) -> bool:
|
||||
logger.info(f"Actual: {actual_hash}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.info(f"⚠️ SHA-256 verification error: {str(e)}")
|
||||
logger.info(f"⚠️ SHA-256 verification error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ def get_ffmpeg_path() -> str | Path:
|
||||
ffmpeg_path = result.stdout.strip()
|
||||
return ffmpeg_path
|
||||
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
|
||||
ffmpeg_install_path = get_ffmpeg_install_path()
|
||||
@@ -171,7 +171,7 @@ def check_ffmpeg_installed() -> bool:
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.info(f"FFmpeg check error: {str(e)}")
|
||||
logger.info(f"FFmpeg check error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ def install_ffmpeg_windows() -> bool:
|
||||
timeout=300,
|
||||
) # 5-minute timeout
|
||||
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
|
||||
else:
|
||||
logger.error("SHA-256 verification failed for 7z file, trying zip fallback...")
|
||||
@@ -236,7 +236,8 @@ def install_ffmpeg_windows() -> bool:
|
||||
temp_file,
|
||||
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...")
|
||||
try:
|
||||
@@ -245,7 +246,8 @@ def install_ffmpeg_windows() -> bool:
|
||||
with zipfile.ZipFile(temp_file, "r") as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
except Exception as e:
|
||||
raise Exception(f"Extraction failed: {str(e)}")
|
||||
logger.exception(f"Extraction failed: {e}")
|
||||
return False
|
||||
|
||||
logger.info("Configuring system paths...")
|
||||
# Add to System Path
|
||||
@@ -265,13 +267,14 @@ def install_ffmpeg_windows() -> bool:
|
||||
|
||||
# Verify installation
|
||||
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!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing FFmpeg: {str(e)}")
|
||||
logger.exception(f"Error installing FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -298,12 +301,13 @@ def install_ffmpeg_macos() -> bool:
|
||||
|
||||
# Verify installation
|
||||
if not check_ffmpeg_installed():
|
||||
raise Exception("FFmpeg installation verification failed")
|
||||
logger.error("FFmpeg installation verification failed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing FFmpeg: {str(e)}")
|
||||
logger.exception(f"Error installing FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -329,16 +333,18 @@ def install_ffmpeg_linux() -> bool:
|
||||
# Universal snap package
|
||||
subprocess.run(["sudo", "snap", "install", "ffmpeg"], check=True, timeout=300)
|
||||
else:
|
||||
raise Exception("No supported package manager found")
|
||||
logger.error("No supported package manager found")
|
||||
return False
|
||||
|
||||
# Verify installation
|
||||
if not check_ffmpeg_installed():
|
||||
raise Exception("FFmpeg installation verification failed")
|
||||
logger.error("FFmpeg installation verification failed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing FFmpeg: {str(e)}")
|
||||
logger.exception(f"Error installing FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -4,28 +4,13 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
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
|
||||
from packaging import version
|
||||
|
||||
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.utils.ytsage_constants import (
|
||||
APP_CONFIG_FILE,
|
||||
@@ -35,6 +20,25 @@ from src.utils.ytsage_constants import (
|
||||
YTDLP_APP_BIN_PATH,
|
||||
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
|
||||
_version_cache = {
|
||||
@@ -108,7 +112,7 @@ def load_version_cache_from_config() -> None:
|
||||
if tool_name in _version_cache:
|
||||
_version_cache[tool_name].update(cache_data)
|
||||
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:
|
||||
@@ -118,7 +122,7 @@ def save_version_cache_to_config() -> None:
|
||||
config["cached_versions"] = _version_cache.copy()
|
||||
save_config(config)
|
||||
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:
|
||||
@@ -140,7 +144,7 @@ def get_ytdlp_version_cached() -> str:
|
||||
|
||||
return version_info
|
||||
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"
|
||||
|
||||
|
||||
@@ -164,7 +168,7 @@ def get_ffmpeg_version_cached() -> str:
|
||||
|
||||
return version_info
|
||||
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"
|
||||
|
||||
|
||||
@@ -182,7 +186,7 @@ def refresh_version_cache(force=False) -> bool:
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing version cache: {e}")
|
||||
logger.exception(f"Error refreshing version cache: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -215,7 +219,7 @@ def get_ytdlp_version_direct(yt_dlp_path=None) -> str:
|
||||
else:
|
||||
return "Error getting version"
|
||||
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"
|
||||
|
||||
|
||||
@@ -269,10 +273,10 @@ def get_ffmpeg_version_direct() -> str:
|
||||
return "Unknown version"
|
||||
return "Not found"
|
||||
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"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting FFmpeg version: {e}")
|
||||
logger.exception(f"Error getting FFmpeg version: {e}")
|
||||
return "Error getting version"
|
||||
|
||||
|
||||
@@ -308,7 +312,7 @@ def load_config() -> dict:
|
||||
config[key] = value
|
||||
return config
|
||||
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
|
||||
save_config(default_config)
|
||||
|
||||
@@ -322,7 +326,7 @@ def save_config(config) -> bool:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving config: {e}")
|
||||
logger.exception(f"Error saving config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -342,7 +346,7 @@ def check_ffmpeg() -> bool:
|
||||
os.environ["PATH"] = f"{ffmpeg_path}{os.pathsep}{os.environ.get('PATH', '')}"
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating PATH: {e}")
|
||||
logger.exception(f"Error updating PATH: {e}")
|
||||
return False
|
||||
|
||||
# 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', '')}"
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating PATH: {e}")
|
||||
logger.exception(f"Error updating PATH: {e}")
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking FFmpeg: {e}")
|
||||
logger.exception(f"Error checking FFmpeg: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -381,7 +385,7 @@ def load_saved_path(main_window_instance) -> None:
|
||||
main_window_instance.last_path = saved_path
|
||||
return
|
||||
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
|
||||
try:
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -409,7 +413,7 @@ def save_path(main_window_instance, path) -> bool:
|
||||
try:
|
||||
Path(path).mkdir(exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating directory: {e}")
|
||||
logger.exception(f"Error creating directory: {e}")
|
||||
return False
|
||||
|
||||
if not os.access(path, os.W_OK):
|
||||
@@ -423,7 +427,7 @@ def save_path(main_window_instance, path) -> bool:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving settings: {e}")
|
||||
logger.exception(f"Error saving settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -484,13 +488,13 @@ def update_yt_dlp() -> bool:
|
||||
logger.info("yt-dlp binary successfully updated")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error replacing yt-dlp binary: {e}")
|
||||
logger.exception(f"Error replacing yt-dlp binary: {e}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading yt-dlp update: {e}")
|
||||
logger.exception(f"Error downloading yt-dlp update: {e}")
|
||||
return False
|
||||
else:
|
||||
# We're using a system-installed yt-dlp, use pip to update
|
||||
@@ -540,9 +544,9 @@ def update_yt_dlp() -> bool:
|
||||
else:
|
||||
logger.info(f"Failed to get latest version info: HTTP {response.status_code}")
|
||||
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:
|
||||
logger.info(f"Unexpected error during yt-dlp update: {e}")
|
||||
logger.exception(f"Unexpected error during yt-dlp update: {e}")
|
||||
|
||||
return False
|
||||
|
||||
@@ -573,7 +577,7 @@ def should_check_for_auto_update() -> bool:
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking auto-update schedule: {e}")
|
||||
logger.exception(f"Error checking auto-update schedule: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -602,9 +606,7 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
logger.info(f"Latest yt-dlp version: {latest_version}")
|
||||
|
||||
# Compare versions
|
||||
from packaging import version as version_parser
|
||||
|
||||
if version_parser.parse(latest_version) > version_parser.parse(current_version):
|
||||
if version.parse(latest_version) > version.parse(current_version):
|
||||
logger.info(f"Auto-updating yt-dlp from {current_version} to {latest_version}...")
|
||||
|
||||
# Perform the update
|
||||
@@ -630,11 +632,11 @@ def check_and_update_ytdlp_auto() -> bool:
|
||||
logger.info(f"Network error during auto-update check: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error during auto-update check: {e}")
|
||||
logger.exception(f"Error during auto-update check: {e}")
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -657,77 +659,103 @@ def update_auto_update_settings(enabled, frequency) -> bool:
|
||||
save_config(config)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating auto-update settings: {e}")
|
||||
logger.exception(f"Error updating auto-update settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def parse_yt_dlp_error(error_message: str) -> str:
|
||||
"""
|
||||
Parse yt-dlp error messages and return user-friendly error messages.
|
||||
|
||||
|
||||
Args:
|
||||
error_message: The raw error message from yt-dlp
|
||||
|
||||
|
||||
Returns:
|
||||
str: A user-friendly error message with actionable advice
|
||||
"""
|
||||
error_str = str(error_message).lower()
|
||||
|
||||
error_str = error_message.lower()
|
||||
|
||||
# Private video errors
|
||||
if any(keyword in error_str for keyword in ['private video', 'login_required', 'sign in if you']):
|
||||
return ("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.")
|
||||
|
||||
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"
|
||||
"Go to 'Custom Options' → 'Login with Cookies' → 'Extract cookies from browser' to authenticate."
|
||||
)
|
||||
|
||||
# Age-restricted content
|
||||
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"
|
||||
"Use 'Custom Options' → 'Login with Cookies' to authenticate with your account.")
|
||||
|
||||
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"
|
||||
"Use 'Custom Options' → 'Login with Cookies' to authenticate with your account."
|
||||
)
|
||||
|
||||
# Geo-blocked content
|
||||
if any(keyword in error_str for keyword in ['not available in your country', 'geo-blocked', 'video is not available', 'not made this video available in your country']):
|
||||
return ("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.")
|
||||
|
||||
if any(
|
||||
keyword in error_str
|
||||
for keyword in [
|
||||
"not available in your country",
|
||||
"geo-blocked",
|
||||
"video is not available",
|
||||
"not made this video available in your country",
|
||||
]
|
||||
):
|
||||
return (
|
||||
"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
|
||||
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"
|
||||
"The video may have been deleted by the uploader or removed due to policy violations.")
|
||||
|
||||
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"
|
||||
"The video may have been deleted by the uploader or removed due to policy violations."
|
||||
)
|
||||
|
||||
# Live stream errors
|
||||
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"
|
||||
"Wait for the stream to end, then try downloading the archived version.")
|
||||
|
||||
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"
|
||||
"Wait for the stream to end, then try downloading the archived version."
|
||||
)
|
||||
|
||||
# Playlist errors
|
||||
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"
|
||||
"Check if the playlist exists and is publicly accessible.")
|
||||
|
||||
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"
|
||||
"Check if the playlist exists and is publicly accessible."
|
||||
)
|
||||
|
||||
# Network/connection errors
|
||||
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"
|
||||
"If the problem persists, the video server might be temporarily unavailable.")
|
||||
|
||||
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"
|
||||
"If the problem persists, the video server might be temporarily unavailable."
|
||||
)
|
||||
|
||||
# Invalid URL
|
||||
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"
|
||||
"Make sure you're using a valid YouTube, Vimeo, or other supported platform URL.")
|
||||
|
||||
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"
|
||||
"Make sure you're using a valid YouTube, Vimeo, or other supported platform URL."
|
||||
)
|
||||
|
||||
# YouTube premium content
|
||||
if any(keyword in error_str for keyword in ['youtube premium', 'premium', 'members only']):
|
||||
return ("This content requires YouTube Premium or channel membership.\n"
|
||||
"You need to be logged in with an account that has access to this content.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["youtube premium", "premium", "members only"]):
|
||||
return (
|
||||
"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
|
||||
if any(keyword in error_str for keyword in ['copyright', 'dmca', 'blocked']):
|
||||
return ("This video is blocked due to copyright claims.\n"
|
||||
"The content owner has restricted access to this video.")
|
||||
|
||||
if any(keyword in error_str for keyword in ["copyright", "dmca", "blocked"]):
|
||||
return "This video is blocked due to copyright claims.\n" "The content owner has restricted access to this video."
|
||||
|
||||
# Extraction errors (could be temporary)
|
||||
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"
|
||||
"Please try again in a few minutes, or check if the video link is correct.")
|
||||
|
||||
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"
|
||||
"Please try again in a few minutes, or check if the video link is correct."
|
||||
)
|
||||
|
||||
# Generic fallback with the original error for debugging
|
||||
return (f"Could not extract video information. Please check your link.\n"
|
||||
f"Technical details: {error_message}")
|
||||
return f"Could not extract video information. Please check your link.\n" f"Technical details: {error_message}"
|
||||
|
||||
@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_constants import (
|
||||
APP_BIN_DIR,
|
||||
ICON_PATH,
|
||||
@@ -94,7 +94,7 @@ class YtdlpSetupDialog(QDialog):
|
||||
# icon_path logic moved to src\utils\ytsage_constants.py
|
||||
icon_path = ICON_PATH
|
||||
if Path.exists(icon_path):
|
||||
self.setWindowIcon(QIcon(icon_path.as_posix()))
|
||||
self.setWindowIcon(QIcon(str(icon_path)))
|
||||
|
||||
self.init_ui()
|
||||
|
||||
@@ -390,11 +390,11 @@ class YtdlpSetupDialog(QDialog):
|
||||
self.setup_complete.emit(target_path)
|
||||
self.accept()
|
||||
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.setIcon(QMessageBox.Icon.Critical)
|
||||
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(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -448,11 +448,11 @@ class YtdlpSetupDialog(QDialog):
|
||||
)
|
||||
error_dialog.exec()
|
||||
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.setIcon(QMessageBox.Icon.Critical)
|
||||
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(
|
||||
"""
|
||||
QMessageBox {
|
||||
@@ -492,7 +492,7 @@ def check_ytdlp_binary() -> Optional[Path]:
|
||||
os.chmod(exe_path, 0o755)
|
||||
logger.info(f"Fixed permissions on yt-dlp at {exe_path}")
|
||||
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
|
||||
|
||||
# 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}")
|
||||
return Path(yt_dlp_path)
|
||||
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
|
||||
|
||||
return None
|
||||
|
||||
@@ -13,10 +13,7 @@ This package contains all dialog classes organized by functionality:
|
||||
|
||||
# Re-export all dialog classes for backward compatibility
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import (
|
||||
CustomOptionsDialog,
|
||||
TimeRangeDialog,
|
||||
)
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog, TimeRangeDialog
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import (
|
||||
PlaylistSelectionDialog,
|
||||
@@ -30,21 +27,21 @@ __all__ = [
|
||||
# Base dialogs
|
||||
"LogWindow",
|
||||
"AboutDialog",
|
||||
|
||||
|
||||
# Settings dialogs
|
||||
"DownloadSettingsDialog",
|
||||
"AutoUpdateSettingsDialog",
|
||||
|
||||
|
||||
# Update dialogs and threads
|
||||
"VersionCheckThread",
|
||||
"UpdateThread",
|
||||
"YTDLPUpdateDialog",
|
||||
"AutoUpdateThread",
|
||||
|
||||
|
||||
# FFmpeg dialogs
|
||||
"FFmpegInstallThread",
|
||||
"FFmpegCheckDialog",
|
||||
|
||||
|
||||
# Selection dialogs
|
||||
"SubtitleSelectionDialog",
|
||||
"PlaylistSelectionDialog",
|
||||
|
||||
@@ -3,6 +3,8 @@ Base dialogs for YTSage application.
|
||||
Contains basic utility dialogs like LogWindow and AboutDialog.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
@@ -17,6 +19,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.utils.ytsage_localization import _
|
||||
|
||||
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_yt_dlp import check_ytdlp_installed, get_yt_dlp_path
|
||||
@@ -58,7 +62,7 @@ class AboutDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
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.resize(460, 440) # Slightly increased initial size
|
||||
self.setMaximumSize(500, 480) # Reasonable maximum size
|
||||
@@ -155,13 +159,13 @@ class AboutDialog(QDialog):
|
||||
layout.addWidget(title_label)
|
||||
|
||||
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)
|
||||
layout.addWidget(version_label)
|
||||
|
||||
# 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.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
|
||||
@@ -172,13 +176,13 @@ class AboutDialog(QDialog):
|
||||
info_layout.setSpacing(15)
|
||||
|
||||
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)
|
||||
info_layout.addWidget(author_label)
|
||||
|
||||
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)
|
||||
info_layout.addWidget(repo_label)
|
||||
@@ -217,7 +221,7 @@ class AboutDialog(QDialog):
|
||||
header_layout.setContentsMargins(0, 0, 0, 5)
|
||||
|
||||
# System Information title
|
||||
title_label = QLabel("System Information")
|
||||
title_label = QLabel(_("about.system_info"))
|
||||
title_label.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
@@ -235,7 +239,7 @@ class AboutDialog(QDialog):
|
||||
header_layout.addStretch()
|
||||
|
||||
# Create refresh button
|
||||
self.refresh_btn = QPushButton("🔄")
|
||||
self.refresh_btn = QPushButton(_("about.refresh"))
|
||||
self.refresh_btn.setFixedSize(16, 16)
|
||||
self.refresh_btn.setStyleSheet(
|
||||
"""
|
||||
@@ -281,7 +285,7 @@ class AboutDialog(QDialog):
|
||||
|
||||
def _show_loading_message(self) -> None:
|
||||
"""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.setStyleSheet(
|
||||
"""
|
||||
@@ -379,7 +383,7 @@ class AboutDialog(QDialog):
|
||||
# yt-dlp Status - compact version with path
|
||||
ytdlp_found = check_ytdlp_installed()
|
||||
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()
|
||||
|
||||
@@ -392,8 +396,6 @@ class AboutDialog(QDialog):
|
||||
last_check = ytdlp_cache.get("last_check", 0)
|
||||
cache_status = ""
|
||||
if last_check > 0:
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
@@ -409,11 +411,11 @@ class AboutDialog(QDialog):
|
||||
# FFmpeg Status - compact version with path
|
||||
ffmpeg_found = check_ffmpeg()
|
||||
ffmpeg_status_text = (
|
||||
"<span style='color: #4CAF50;'>✓ Detected</span>"
|
||||
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
|
||||
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
|
||||
ffmpeg_path_text = None
|
||||
@@ -426,8 +428,6 @@ class AboutDialog(QDialog):
|
||||
last_check = ffmpeg_cache.get("last_check", 0)
|
||||
cache_status = ""
|
||||
if last_check > 0 and ffmpeg_found:
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
@@ -442,7 +442,7 @@ class AboutDialog(QDialog):
|
||||
|
||||
def refresh_version_info(self) -> None:
|
||||
"""Refresh version information manually."""
|
||||
self.refresh_btn.setText("🔄 Refreshing...")
|
||||
self.refresh_btn.setText(_('about.refreshing'))
|
||||
self.refresh_btn.setEnabled(False)
|
||||
|
||||
# Perform refresh in a separate thread to avoid blocking UI
|
||||
@@ -459,7 +459,7 @@ class AboutDialog(QDialog):
|
||||
|
||||
def on_refresh_finished(self, success) -> None:
|
||||
"""Handle refresh completion."""
|
||||
self.refresh_btn.setText("🔄 Refresh")
|
||||
self.refresh_btn.setText(_('about.refresh'))
|
||||
self.refresh_btn.setEnabled(True)
|
||||
|
||||
if success:
|
||||
@@ -468,8 +468,8 @@ class AboutDialog(QDialog):
|
||||
# Show error message with proper styling
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
msg_box.setWindowTitle("Refresh Failed")
|
||||
msg_box.setText("Failed to refresh version information.")
|
||||
msg_box.setWindowTitle(_('about.refresh_failed'))
|
||||
msg_box.setText(_('about.refresh_failed_message'))
|
||||
msg_box.setWindowIcon(self.windowIcon())
|
||||
msg_box.setStyleSheet(
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,7 @@ import threading
|
||||
from pathlib import Path
|
||||
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 (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
@@ -30,13 +30,9 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import YTDLP_DOCS_URL
|
||||
|
||||
try:
|
||||
import yt_dlp
|
||||
|
||||
YT_DLP_AVAILABLE = True
|
||||
except ImportError:
|
||||
YT_DLP_AVAILABLE = False
|
||||
from src.utils.ytsage_config_manager import ConfigManager
|
||||
from src.utils.ytsage_localization import LocalizationManager, _
|
||||
from src.utils.ytsage_logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
|
||||
@@ -44,18 +40,18 @@ if TYPE_CHECKING:
|
||||
|
||||
class CommandWorker(QObject):
|
||||
"""Worker class for running yt-dlp commands in a separate thread"""
|
||||
|
||||
|
||||
# Signals for communicating with the main thread
|
||||
output_received = Signal(str) # For command output lines
|
||||
command_finished = Signal(bool, int) # For completion (success, exit_code)
|
||||
error_occurred = Signal(str) # For errors
|
||||
|
||||
|
||||
def __init__(self, command, url, path):
|
||||
super().__init__()
|
||||
self.command = command
|
||||
self.url = url
|
||||
self.path = path
|
||||
|
||||
|
||||
def run_command(self):
|
||||
"""Run the yt-dlp command and emit signals for output"""
|
||||
try:
|
||||
@@ -65,16 +61,16 @@ class CommandWorker(QObject):
|
||||
# Build the full command
|
||||
yt_dlp_path = get_yt_dlp_path()
|
||||
base_cmd = [yt_dlp_path] + args
|
||||
|
||||
|
||||
# Add download path if specified
|
||||
if self.path:
|
||||
base_cmd.extend(["-P", self.path])
|
||||
|
||||
|
||||
# Add URL at the end
|
||||
base_cmd.append(self.url)
|
||||
|
||||
# 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)
|
||||
|
||||
# Run the command
|
||||
@@ -94,24 +90,24 @@ class CommandWorker(QObject):
|
||||
|
||||
ret = proc.wait()
|
||||
self.output_received.emit("=" * 50)
|
||||
|
||||
|
||||
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)
|
||||
else:
|
||||
self.output_received.emit("✅ Command completed successfully!")
|
||||
self.output_received.emit(_('custom_command.command_success'))
|
||||
self.command_finished.emit(True, ret)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
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):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
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
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
@@ -124,47 +120,44 @@ class CustomOptionsDialog(QDialog):
|
||||
cookies_layout = QVBoxLayout(cookies_tab)
|
||||
|
||||
# Help text
|
||||
help_text = QLabel(
|
||||
"Choose how to provide cookies for logging in.\n"
|
||||
"This allows downloading of private videos and premium quality audio."
|
||||
)
|
||||
help_text = QLabel(_('cookies.help_text'))
|
||||
help_text.setWordWrap(True)
|
||||
help_text.setStyleSheet("color: #999999; padding: 10px;")
|
||||
cookies_layout.addWidget(help_text)
|
||||
|
||||
# Cookie source selection
|
||||
cookie_source_group = QGroupBox("Cookie Source")
|
||||
cookie_source_group = QGroupBox(_('cookies.cookie_source'))
|
||||
cookie_source_layout = QVBoxLayout(cookie_source_group)
|
||||
|
||||
# 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.toggled.connect(self.on_cookie_source_changed)
|
||||
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)
|
||||
cookie_source_layout.addWidget(self.cookie_browser_radio)
|
||||
|
||||
cookies_layout.addWidget(cookie_source_group)
|
||||
|
||||
# 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 path input and browse button
|
||||
path_layout = QHBoxLayout()
|
||||
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:
|
||||
# Convert Path to string properly and validate
|
||||
cookie_path_str = str(self._parent.cookie_file_path)
|
||||
# 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)
|
||||
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)
|
||||
path_layout.addWidget(self.browse_button)
|
||||
file_layout.addLayout(path_layout)
|
||||
@@ -172,38 +165,27 @@ class CustomOptionsDialog(QDialog):
|
||||
cookies_layout.addWidget(self.cookie_file_group)
|
||||
|
||||
# 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_help = QLabel(
|
||||
"Select the browser to extract cookies from. Make sure the browser is closed before extraction."
|
||||
)
|
||||
browser_help = QLabel(_('cookies.browser_help'))
|
||||
browser_help.setWordWrap(True)
|
||||
browser_help.setStyleSheet("color: #999999; font-size: 11px;")
|
||||
browser_layout.addWidget(browser_help)
|
||||
|
||||
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.addItems([
|
||||
"chrome",
|
||||
"firefox",
|
||||
"safari",
|
||||
"edge",
|
||||
"opera",
|
||||
"brave",
|
||||
"chromium",
|
||||
"vivaldi"
|
||||
])
|
||||
self.browser_combo.addItems(["chrome", "firefox", "safari", "edge", "opera", "brave", "chromium", "vivaldi"])
|
||||
browser_select_layout.addWidget(self.browser_combo)
|
||||
browser_layout.addLayout(browser_select_layout)
|
||||
|
||||
# Optional profile field
|
||||
profile_layout = QHBoxLayout()
|
||||
profile_layout.addWidget(QLabel("Profile (optional):"))
|
||||
profile_layout.addWidget(QLabel(_('cookies.profile_label')))
|
||||
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)
|
||||
browser_layout.addLayout(profile_layout)
|
||||
|
||||
@@ -224,12 +206,7 @@ class CustomOptionsDialog(QDialog):
|
||||
command_layout = QVBoxLayout(command_tab)
|
||||
|
||||
# Improved help text
|
||||
cmd_help_text = QLabel(
|
||||
"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 = QLabel(_('custom_command.help_text', docs_url=YTDLP_DOCS_URL))
|
||||
cmd_help_text.setWordWrap(True)
|
||||
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
|
||||
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
|
||||
@@ -255,16 +232,13 @@ class CustomOptionsDialog(QDialog):
|
||||
command_layout.addWidget(cmd_help_text)
|
||||
|
||||
# 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;")
|
||||
command_layout.addWidget(input_label)
|
||||
|
||||
# Command input
|
||||
self.command_input = QPlainTextEdit()
|
||||
self.command_input.setPlaceholderText(
|
||||
"Enter yt-dlp arguments here...\n\n"
|
||||
"e.g. --extract-audio --audio-format mp3"
|
||||
)
|
||||
self.command_input.setPlaceholderText(_('custom_command.input_placeholder'))
|
||||
self.command_input.setMinimumHeight(80) # Reduced further from 100
|
||||
self.command_input.setStyleSheet(
|
||||
"""
|
||||
@@ -288,8 +262,8 @@ class CustomOptionsDialog(QDialog):
|
||||
# Button layout
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
clear_btn = QPushButton("Clear")
|
||||
|
||||
clear_btn = QPushButton(_('buttons.clear'))
|
||||
clear_btn.clicked.connect(lambda: self.command_input.clear())
|
||||
clear_btn.setStyleSheet(
|
||||
"""
|
||||
@@ -308,26 +282,26 @@ class CustomOptionsDialog(QDialog):
|
||||
"""
|
||||
)
|
||||
button_layout.addWidget(clear_btn)
|
||||
|
||||
|
||||
button_layout.addStretch() # Push run button to the right
|
||||
|
||||
|
||||
# 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.setDefault(True)
|
||||
button_layout.addWidget(self.run_btn)
|
||||
|
||||
|
||||
command_layout.addLayout(button_layout)
|
||||
|
||||
# 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;")
|
||||
command_layout.addWidget(output_label)
|
||||
|
||||
# Log output
|
||||
self.log_output = QTextEdit()
|
||||
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.setStyleSheet(
|
||||
"""
|
||||
@@ -347,12 +321,180 @@ class CustomOptionsDialog(QDialog):
|
||||
)
|
||||
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
|
||||
self.tab_widget.addTab(cookies_tab, "Login with Cookies")
|
||||
self.tab_widget.addTab(command_tab, "Custom Command")
|
||||
self.tab_widget.addTab(cookies_tab, _("tabs.cookies"))
|
||||
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
|
||||
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.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
@@ -432,7 +574,6 @@ class CustomOptionsDialog(QDialog):
|
||||
border: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1d1e22;
|
||||
@@ -463,24 +604,25 @@ class CustomOptionsDialog(QDialog):
|
||||
|
||||
# Initialize dialog with current settings (after all widgets and styles are set)
|
||||
self._initialize_cookie_settings()
|
||||
self._initialize_proxy_settings()
|
||||
|
||||
def _initialize_cookie_settings(self) -> None:
|
||||
"""Initialize the dialog with current cookie settings from parent"""
|
||||
if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option:
|
||||
# Browser cookies are active
|
||||
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]
|
||||
profile = browser_parts[1] if len(browser_parts) > 1 else ""
|
||||
|
||||
|
||||
# Set browser selection
|
||||
index = self.browser_combo.findText(browser)
|
||||
if index >= 0:
|
||||
self.browser_combo.setCurrentIndex(index)
|
||||
|
||||
|
||||
# Set profile if any
|
||||
self.profile_input.setText(profile)
|
||||
|
||||
|
||||
self.cookie_status.setText(f"Browser cookies active: {self._parent.browser_cookies_option}")
|
||||
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
|
||||
elif hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
|
||||
@@ -492,6 +634,22 @@ class CustomOptionsDialog(QDialog):
|
||||
# No cookies configured - ensure file radio is selected by default
|
||||
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:
|
||||
"""Handle cookie source radio button changes"""
|
||||
if self.cookie_file_radio.isChecked():
|
||||
@@ -501,18 +659,18 @@ class CustomOptionsDialog(QDialog):
|
||||
else:
|
||||
self.cookie_file_group.setVisible(False)
|
||||
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;")
|
||||
|
||||
def browse_cookie_file(self) -> None:
|
||||
# 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:
|
||||
# Ensure we have a valid full path
|
||||
cookie_path = Path(selected_files).resolve()
|
||||
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;")
|
||||
|
||||
def get_cookie_file_path(self) -> Path | None:
|
||||
@@ -533,7 +691,7 @@ class CustomOptionsDialog(QDialog):
|
||||
if self.cookie_browser_radio.isChecked():
|
||||
browser = self.browser_combo.currentText()
|
||||
profile = self.profile_input.text().strip()
|
||||
|
||||
|
||||
if profile:
|
||||
return f"{browser}:{profile}"
|
||||
else:
|
||||
@@ -544,6 +702,91 @@ class CustomOptionsDialog(QDialog):
|
||||
"""Returns True if browser cookies mode is selected"""
|
||||
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:
|
||||
url = self._parent.url_input.text().strip()
|
||||
if not url:
|
||||
@@ -566,17 +809,17 @@ class CustomOptionsDialog(QDialog):
|
||||
self.log_output.append(f"📁 Download path: {path}")
|
||||
self.log_output.append("=" * 50)
|
||||
self.run_btn.setEnabled(False)
|
||||
self.run_btn.setText("Running...")
|
||||
self.run_btn.setText(_("command.running"))
|
||||
|
||||
# Create worker and thread
|
||||
self.worker = CommandWorker(command, url, path)
|
||||
self.worker_thread = threading.Thread(target=self.worker.run_command, daemon=True)
|
||||
|
||||
|
||||
# Connect worker signals to our slots
|
||||
self.worker.output_received.connect(self.on_output_received)
|
||||
self.worker.command_finished.connect(self.on_command_finished)
|
||||
self.worker.error_occurred.connect(self.on_error_occurred)
|
||||
|
||||
|
||||
# Start the thread
|
||||
self.worker_thread.start()
|
||||
|
||||
@@ -587,49 +830,63 @@ class CustomOptionsDialog(QDialog):
|
||||
def on_command_finished(self, success: bool, exit_code: int):
|
||||
"""Slot for when command finishes"""
|
||||
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):
|
||||
"""Slot for handling errors"""
|
||||
self.log_output.append(error_msg)
|
||||
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):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Download Video Section")
|
||||
self.setWindowTitle(_('time_range.title'))
|
||||
self.setMinimumWidth(400)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Help text explaining the feature
|
||||
help_text = QLabel(
|
||||
"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 = QLabel(_('time_range.help_text'))
|
||||
help_text.setWordWrap(True)
|
||||
help_text.setStyleSheet("color: #999999; padding: 10px;")
|
||||
layout.addWidget(help_text)
|
||||
|
||||
# Time range section
|
||||
time_group = QGroupBox("Time Range")
|
||||
time_group = QGroupBox(_('time_range.time_range_group'))
|
||||
time_layout = QVBoxLayout()
|
||||
|
||||
# Start time row
|
||||
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.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)
|
||||
time_layout.addLayout(start_layout)
|
||||
|
||||
# End time row
|
||||
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.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)
|
||||
time_layout.addLayout(end_layout)
|
||||
|
||||
@@ -637,7 +894,7 @@ class TimeRangeDialog(QDialog):
|
||||
layout.addWidget(time_group)
|
||||
|
||||
# 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.setStyleSheet(
|
||||
"""
|
||||
@@ -665,7 +922,9 @@ class TimeRangeDialog(QDialog):
|
||||
layout.addWidget(self.force_keyframes)
|
||||
|
||||
# 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.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
@@ -48,7 +48,7 @@ class FFmpegCheckDialog(QDialog):
|
||||
# icon_path logic moved to src\utils\ytsage_constants.py
|
||||
|
||||
if ICON_PATH.exists():
|
||||
self.setWindowIcon(QIcon(ICON_PATH.as_posix()))
|
||||
self.setWindowIcon(QIcon(str(ICON_PATH)))
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15)
|
||||
|
||||
@@ -17,11 +17,13 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.utils.ytsage_localization import _
|
||||
|
||||
|
||||
class SubtitleSelectionDialog(QDialog):
|
||||
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Select Subtitles")
|
||||
self.setWindowTitle(_("dialogs.select_subtitles"))
|
||||
self.setMinimumWidth(400)
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
@@ -35,7 +37,7 @@ class SubtitleSelectionDialog(QDialog):
|
||||
|
||||
# Filter input
|
||||
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.setStyleSheet(
|
||||
"""
|
||||
@@ -72,7 +74,9 @@ class SubtitleSelectionDialog(QDialog):
|
||||
self.populate_list()
|
||||
|
||||
# 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.rejected.connect(self.reject)
|
||||
|
||||
@@ -128,7 +132,8 @@ class SubtitleSelectionDialog(QDialog):
|
||||
combined_subs[lang_code] = f"{lang_code} - Auto-generated"
|
||||
|
||||
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;")
|
||||
self.list_layout.addWidget(no_subs_label)
|
||||
return
|
||||
@@ -193,7 +198,7 @@ class SubtitleSelectionDialog(QDialog):
|
||||
class PlaylistSelectionDialog(QDialog):
|
||||
def __init__(self, playlist_entries, previously_selected_string, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Select Playlist Videos")
|
||||
self.setWindowTitle(_("playlist.select_videos_title"))
|
||||
self.setMinimumWidth(500)
|
||||
self.setMinimumHeight(400) # Allow more vertical space
|
||||
|
||||
@@ -205,8 +210,8 @@ class PlaylistSelectionDialog(QDialog):
|
||||
|
||||
# Top buttons (Select/Deselect All)
|
||||
button_layout = QHBoxLayout()
|
||||
select_all_btn = QPushButton("Select All")
|
||||
deselect_all_btn = QPushButton("Deselect All")
|
||||
select_all_btn = QPushButton(_("buttons.select_all"))
|
||||
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
|
||||
select_all_btn.clicked.connect(self._select_all)
|
||||
deselect_all_btn.clicked.connect(self._deselect_all)
|
||||
# Style the buttons to match the subtitle dialog
|
||||
@@ -250,7 +255,9 @@ class PlaylistSelectionDialog(QDialog):
|
||||
self._populate_list(previously_selected_string)
|
||||
|
||||
# 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.rejected.connect(self.reject)
|
||||
|
||||
@@ -428,50 +435,50 @@ class SponsorBlockCategoryDialog(QDialog):
|
||||
# Default SponsorBlock categories with descriptions
|
||||
SPONSORBLOCK_CATEGORIES = {
|
||||
"sponsor": {
|
||||
"name": "Sponsor",
|
||||
"description": "Paid promotion, paid referrals and direct advertisements",
|
||||
"name_key": "sponsorblock.sponsor",
|
||||
"description_key": "sponsorblock.sponsor_desc",
|
||||
"default": True,
|
||||
},
|
||||
"selfpromo": {
|
||||
"name": "Unpaid/Self Promotion",
|
||||
"description": "Unpaid promotion of creators' own content",
|
||||
"name_key": "sponsorblock.selfpromo",
|
||||
"description_key": "sponsorblock.selfpromo_desc",
|
||||
"default": True,
|
||||
},
|
||||
"interaction": {
|
||||
"name": "Interaction Reminder",
|
||||
"description": "Asking viewers to like, subscribe, or follow social media",
|
||||
"name_key": "sponsorblock.interaction",
|
||||
"description_key": "sponsorblock.interaction_desc",
|
||||
"default": True,
|
||||
},
|
||||
"intro": {
|
||||
"name": "Intro",
|
||||
"description": "Video introduction that can be skipped",
|
||||
"name_key": "sponsorblock.intro",
|
||||
"description_key": "sponsorblock.intro_desc",
|
||||
"default": False,
|
||||
},
|
||||
"outro": {
|
||||
"name": "Outro/End Cards",
|
||||
"description": "Credits or when the video ends",
|
||||
"name_key": "sponsorblock.outro",
|
||||
"description_key": "sponsorblock.outro_desc",
|
||||
"default": False,
|
||||
},
|
||||
"preview": {
|
||||
"name": "Preview/Recap",
|
||||
"description": "Quick recap of previous videos or preview of what's coming up",
|
||||
"name_key": "sponsorblock.preview",
|
||||
"description_key": "sponsorblock.preview_desc",
|
||||
"default": False,
|
||||
},
|
||||
"music_offtopic": {
|
||||
"name": "Non-Music Section",
|
||||
"description": "Only for music videos. Marks non-music sections",
|
||||
"name_key": "sponsorblock.music_offtopic",
|
||||
"description_key": "sponsorblock.music_offtopic_desc",
|
||||
"default": False,
|
||||
},
|
||||
"filler": {
|
||||
"name": "Filler Tangent",
|
||||
"description": "Tangential scenes added only for filler or humor",
|
||||
"name_key": "sponsorblock.filler",
|
||||
"description_key": "sponsorblock.filler_desc",
|
||||
"default": False,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, previously_selected=None, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("SponsorBlock Categories")
|
||||
self.setWindowTitle(_("dialogs.sponsorblock_categories"))
|
||||
self.setMinimumWidth(500)
|
||||
self.setMinimumHeight(400)
|
||||
|
||||
@@ -489,15 +496,12 @@ class SponsorBlockCategoryDialog(QDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Title and description
|
||||
title_label = QLabel("SponsorBlock Categories")
|
||||
title_label = QLabel(_("dialogs.sponsorblock_categories"))
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
desc_label = QLabel(
|
||||
"Select which types of video segments to automatically remove during download.\n"
|
||||
"SponsorBlock uses community-submitted data to identify these segments."
|
||||
)
|
||||
desc_label = QLabel(_("dialogs.sponsorblock_description"))
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
|
||||
@@ -521,8 +525,8 @@ class SponsorBlockCategoryDialog(QDialog):
|
||||
category_layout.setContentsMargins(0, 0, 0, 0)
|
||||
category_layout.setSpacing(2)
|
||||
|
||||
# Create checkbox with just the name
|
||||
checkbox = QCheckBox(category_info["name"])
|
||||
# Create checkbox with localized name
|
||||
checkbox = QCheckBox(_(category_info["name_key"]))
|
||||
checkbox.setProperty("category_id", category_id)
|
||||
|
||||
# Determine if this category should be checked
|
||||
@@ -559,8 +563,8 @@ class SponsorBlockCategoryDialog(QDialog):
|
||||
"""
|
||||
)
|
||||
|
||||
# Create description label
|
||||
desc_label = QLabel(category_info["description"])
|
||||
# Create description label with localized text
|
||||
desc_label = QLabel(_(category_info["description_key"]))
|
||||
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
|
||||
desc_label.setWordWrap(True)
|
||||
|
||||
@@ -577,15 +581,15 @@ class SponsorBlockCategoryDialog(QDialog):
|
||||
# Quick selection buttons
|
||||
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.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.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.setStyleSheet(self._get_button_style())
|
||||
|
||||
@@ -597,7 +601,9 @@ class SponsorBlockCategoryDialog(QDialog):
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 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.rejected.connect(self.reject)
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ Settings-related dialogs for YTSage application.
|
||||
Contains dialogs for configuring download settings and auto-update preferences.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from PySide6.QtCore import Qt
|
||||
from packaging import version as version_parser
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
@@ -25,19 +27,20 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_utils import (
|
||||
check_and_update_ytdlp_auto,
|
||||
get_auto_update_settings,
|
||||
get_ytdlp_version,
|
||||
update_auto_update_settings,
|
||||
)
|
||||
from src.utils.ytsage_logger import logger
|
||||
from src.utils.ytsage_localization import _
|
||||
|
||||
|
||||
class DownloadSettingsDialog(QDialog):
|
||||
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Download Settings")
|
||||
self.setWindowTitle(_("settings.title"))
|
||||
self.setMinimumWidth(450)
|
||||
self.setMinimumHeight(400)
|
||||
self.current_path = current_path
|
||||
@@ -161,17 +164,18 @@ class DownloadSettingsDialog(QDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# --- Download Path Section ---
|
||||
path_group_box = QGroupBox("Download Path")
|
||||
path_group_box = QGroupBox(_("settings.download_path"))
|
||||
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.setStyleSheet(
|
||||
"QLabel { color: #ffffff; padding: 5px; border: 1px solid #1b2021; border-radius: 4px; background-color: #1b2021; }"
|
||||
)
|
||||
path_layout.addWidget(self.path_display)
|
||||
|
||||
browse_button = QPushButton("Browse...")
|
||||
|
||||
browse_button = QPushButton(_("settings.browse"))
|
||||
browse_button.clicked.connect(self.browse_new_path)
|
||||
path_layout.addWidget(browse_button)
|
||||
|
||||
@@ -179,11 +183,11 @@ class DownloadSettingsDialog(QDialog):
|
||||
layout.addWidget(path_group_box)
|
||||
|
||||
# --- Speed Limit Section ---
|
||||
speed_group_box = QGroupBox("Speed Limit")
|
||||
speed_group_box = QGroupBox(_("settings.speed_limit"))
|
||||
speed_layout = QHBoxLayout()
|
||||
|
||||
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)
|
||||
|
||||
self.speed_limit_unit = QComboBox()
|
||||
@@ -195,25 +199,25 @@ class DownloadSettingsDialog(QDialog):
|
||||
layout.addWidget(speed_group_box)
|
||||
|
||||
# --- 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()
|
||||
|
||||
# Load current auto-update settings
|
||||
auto_settings = get_auto_update_settings()
|
||||
|
||||
# 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"])
|
||||
auto_update_layout.addWidget(self.auto_update_enabled)
|
||||
|
||||
# Frequency options
|
||||
frequency_label = QLabel("Update frequency:")
|
||||
frequency_label = QLabel(_("settings.update_frequency"))
|
||||
frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
|
||||
auto_update_layout.addWidget(frequency_label)
|
||||
|
||||
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
|
||||
self.daily_radio = QRadioButton("Check daily")
|
||||
self.weekly_radio = QRadioButton("Check weekly")
|
||||
self.startup_radio = QRadioButton(_("settings.check_startup"))
|
||||
self.daily_radio = QRadioButton(_("settings.check_daily"))
|
||||
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
|
||||
|
||||
# Set current selection based on saved settings
|
||||
current_frequency = auto_settings["frequency"]
|
||||
@@ -230,7 +234,7 @@ class DownloadSettingsDialog(QDialog):
|
||||
|
||||
# Test update button
|
||||
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_layout.addWidget(test_update_button)
|
||||
test_update_layout.addStretch()
|
||||
@@ -240,13 +244,15 @@ class DownloadSettingsDialog(QDialog):
|
||||
layout.addWidget(auto_update_group_box)
|
||||
|
||||
# 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.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
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:
|
||||
self.current_path = new_path
|
||||
self.path_display.setText(self.current_path)
|
||||
@@ -314,8 +320,8 @@ class DownloadSettingsDialog(QDialog):
|
||||
if "Error" in current_version:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Update Check",
|
||||
"Could not determine current yt-dlp version.",
|
||||
_("settings.update_check_title"),
|
||||
_("settings.could_not_determine_version"),
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
@@ -329,27 +335,25 @@ class DownloadSettingsDialog(QDialog):
|
||||
current_version = current_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):
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Update Check",
|
||||
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_check_title"),
|
||||
_("settings.update_available_dialog", current=current_version, latest=latest_version),
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Update Check",
|
||||
f"yt-dlp is up to date!\n\nCurrent version: {current_version}",
|
||||
_("settings.update_check_title"),
|
||||
_("settings.up_to_date_dialog", version=current_version),
|
||||
)
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Update Check",
|
||||
f"Error checking for updates: {str(e)}",
|
||||
_("settings.update_check_title"),
|
||||
_("settings.error_checking_updates", error=str(e)),
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
@@ -375,13 +379,13 @@ class DownloadSettingsDialog(QDialog):
|
||||
if update_auto_update_settings(enabled, frequency):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Settings Saved",
|
||||
"Auto-update settings have been saved successfully!",
|
||||
_("settings.settings_saved_title"),
|
||||
_("settings.settings_saved_message"),
|
||||
)
|
||||
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:
|
||||
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
|
||||
super().accept()
|
||||
@@ -390,7 +394,7 @@ class DownloadSettingsDialog(QDialog):
|
||||
class AutoUpdateSettingsDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Auto-Update Settings")
|
||||
self.setWindowTitle(_("settings.auto_update_title"))
|
||||
self.setMinimumWidth(400)
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
@@ -406,32 +410,32 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 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)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 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.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
# Enable/Disable auto-update
|
||||
self.enable_checkbox = QCheckBox("Enable automatic yt-dlp updates")
|
||||
self.enable_checkbox = QCheckBox(_("settings.enable_auto_updates"))
|
||||
self.enable_checkbox.setChecked(True) # Default enabled
|
||||
self.enable_checkbox.toggled.connect(self.on_enable_toggled)
|
||||
layout.addWidget(self.enable_checkbox)
|
||||
|
||||
# Frequency options
|
||||
frequency_group = QGroupBox("Update Frequency")
|
||||
frequency_group = QGroupBox(_("settings.update_frequency_group"))
|
||||
frequency_layout = QVBoxLayout()
|
||||
|
||||
self.frequency_group = QButtonGroup(self)
|
||||
|
||||
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
|
||||
self.daily_radio = QRadioButton("Check daily")
|
||||
self.weekly_radio = QRadioButton("Check weekly")
|
||||
self.startup_radio = QRadioButton(_("settings.check_startup"))
|
||||
self.daily_radio = QRadioButton(_("settings.check_daily"))
|
||||
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
|
||||
|
||||
self.daily_radio.setChecked(True) # Default to daily
|
||||
|
||||
@@ -447,12 +451,12 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
layout.addWidget(frequency_group)
|
||||
|
||||
# Current status
|
||||
status_group = QGroupBox("Current Status")
|
||||
status_group = QGroupBox(_("settings.current_status"))
|
||||
status_layout = QVBoxLayout()
|
||||
|
||||
self.current_version_label = QLabel("Current yt-dlp version: Checking...")
|
||||
self.last_check_label = QLabel("Last update check: Never")
|
||||
self.next_check_label = QLabel("Next check: Based on settings")
|
||||
self.current_version_label = QLabel(_("settings.current_version_label"))
|
||||
self.last_check_label = QLabel(_("settings.last_check_label"))
|
||||
self.next_check_label = QLabel(_("settings.next_check_label"))
|
||||
|
||||
status_layout.addWidget(self.current_version_label)
|
||||
status_layout.addWidget(self.last_check_label)
|
||||
@@ -462,17 +466,17 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
layout.addWidget(status_group)
|
||||
|
||||
# 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)
|
||||
layout.addWidget(self.manual_check_btn)
|
||||
|
||||
# Buttons
|
||||
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.cancel_btn = QPushButton("Cancel")
|
||||
self.cancel_btn = QPushButton(_("buttons.cancel"))
|
||||
self.cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addWidget(self.save_btn)
|
||||
@@ -571,9 +575,9 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
last_check = settings["last_check"]
|
||||
if last_check > 0:
|
||||
last_check_time = datetime.fromtimestamp(last_check).strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.last_check_label.setText(f"Last update check: {last_check_time}")
|
||||
self.last_check_label.setText(_("auto_update.last_check", time=last_check_time))
|
||||
else:
|
||||
self.last_check_label.setText("Last update check: Never")
|
||||
self.last_check_label.setText(_("auto_update.last_check_never"))
|
||||
|
||||
# Calculate next check time
|
||||
self.update_next_check_label()
|
||||
@@ -582,13 +586,13 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
self.on_enable_toggled(settings["enabled"])
|
||||
|
||||
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:
|
||||
"""Update the next check label based on current settings."""
|
||||
try:
|
||||
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
|
||||
|
||||
settings = get_auto_update_settings()
|
||||
@@ -596,7 +600,7 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
frequency = self.get_selected_frequency()
|
||||
|
||||
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
|
||||
|
||||
next_check_time = last_check
|
||||
@@ -609,14 +613,14 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
|
||||
current_time = time.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:
|
||||
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:
|
||||
self.next_check_label.setText("Next check: Error calculating")
|
||||
logger.error(f"Error calculating next check time: {e}")
|
||||
self.next_check_label.setText(_("auto_update.next_check_error"))
|
||||
logger.exception(f"Error calculating next check time: {e}")
|
||||
|
||||
def on_enable_toggled(self, enabled) -> None:
|
||||
"""Handle enable/disable checkbox toggle."""
|
||||
@@ -638,7 +642,7 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
def manual_check(self) -> None:
|
||||
"""Perform a manual update check."""
|
||||
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
|
||||
def check_in_thread() -> None:
|
||||
@@ -646,16 +650,12 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
result = check_and_update_ytdlp_auto()
|
||||
|
||||
# Update UI in main thread
|
||||
from PySide6.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(0, lambda: self.manual_check_finished(result))
|
||||
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))
|
||||
|
||||
# Run in separate thread to avoid blocking UI
|
||||
import threading
|
||||
|
||||
threading.Thread(target=check_in_thread, daemon=True).start()
|
||||
|
||||
def _create_styled_message_box(self, icon, title, text) -> QMessageBox:
|
||||
@@ -696,7 +696,7 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
def manual_check_finished(self, success) -> None:
|
||||
"""Handle completion of manual update check."""
|
||||
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:
|
||||
msg_box = self._create_styled_message_box(
|
||||
@@ -725,19 +725,19 @@ class AutoUpdateSettingsDialog(QDialog):
|
||||
if update_auto_update_settings(enabled, frequency):
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Settings Saved",
|
||||
"✅ Auto-update settings have been saved successfully!",
|
||||
_("settings.settings_saved_title"),
|
||||
_("settings.settings_saved_successfully"),
|
||||
)
|
||||
msg_box.exec()
|
||||
self.accept()
|
||||
else:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Error",
|
||||
"❌ Failed to save auto-update settings.\nPlease try again.",
|
||||
_("settings.error_title"),
|
||||
_("settings.failed_save_settings"),
|
||||
)
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving auto-update settings: {e}")
|
||||
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, "Error", f"❌ Error saving settings: {str(e)}")
|
||||
logger.exception(f"Error saving auto-update settings: {e}")
|
||||
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, _("settings.error_title"), _("settings", "error_saving", error=str(e)))
|
||||
msg_box.exec()
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
@@ -14,24 +15,31 @@ from packaging import version
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
|
||||
from src.utils.ytsage_localization import LocalizationManager
|
||||
|
||||
# Shorthand for localization
|
||||
_ = LocalizationManager.get_text
|
||||
from src.utils.ytsage_localization import _
|
||||
from src.utils.ytsage_logger import logger
|
||||
|
||||
try:
|
||||
from importlib.metadata import version as importlib_version
|
||||
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
|
||||
|
||||
try:
|
||||
@@ -71,7 +79,7 @@ class VersionCheckThread(QThread):
|
||||
else:
|
||||
error_message = "yt-dlp not available."
|
||||
self.finished.emit(current_version, latest_version, error_message)
|
||||
return
|
||||
return
|
||||
except subprocess.TimeoutExpired:
|
||||
# Try fallback if timeout
|
||||
if YT_DLP_AVAILABLE:
|
||||
@@ -115,16 +123,16 @@ class UpdateThread(QThread):
|
||||
error_message = ""
|
||||
success = False
|
||||
try:
|
||||
self.update_status.emit("🔍 Checking current installation...")
|
||||
self.update_status.emit(_('update.checking_current'))
|
||||
self.update_progress.emit(10)
|
||||
|
||||
# Get the yt-dlp path
|
||||
try:
|
||||
yt_dlp_path = get_yt_dlp_path()
|
||||
self.update_status.emit(f"📍 Found yt-dlp at: {yt_dlp_path}")
|
||||
self.update_status.emit(_('update.found_at', path=yt_dlp_path))
|
||||
except Exception as e:
|
||||
self.update_status.emit(f"❌ Error getting yt-dlp path: {e}")
|
||||
self.update_finished.emit(False, f"❌ Error getting yt-dlp path: {e}")
|
||||
self.update_status.emit(_('update.error_getting_path', error=e))
|
||||
self.update_finished.emit(False, _('update.error_getting_path', error=e))
|
||||
return
|
||||
|
||||
# Extra logic moved to src\utils\ytsage_constants.py
|
||||
@@ -149,24 +157,24 @@ class UpdateThread(QThread):
|
||||
is_app_managed = False
|
||||
|
||||
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)
|
||||
else:
|
||||
self.update_status.emit("🐍 Updating system yt-dlp via pip...")
|
||||
self.update_status.emit(_('update.updating_pip'))
|
||||
success = self._update_via_pip()
|
||||
|
||||
if success:
|
||||
self.update_progress.emit(100)
|
||||
error_message = "✅ yt-dlp has been successfully updated!"
|
||||
error_message = _('update.update_success')
|
||||
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:
|
||||
error_message = f"❌ Network error during update: {str(e)}"
|
||||
error_message = _('update.network_error', error=e)
|
||||
self.update_status.emit(error_message)
|
||||
success = False
|
||||
except Exception as e:
|
||||
error_message = f"❌ Update failed: {str(e)}"
|
||||
error_message = _('update.general_error', error=e)
|
||||
self.update_status.emit(error_message)
|
||||
success = False
|
||||
|
||||
@@ -193,61 +201,61 @@ class UpdateThread(QThread):
|
||||
logger.info("UpdateThread: yt-dlp update completed successfully.")
|
||||
if result.stdout:
|
||||
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)
|
||||
return True
|
||||
else:
|
||||
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
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"UpdateThread: Unexpected error during update: {e}", exc_info=True)
|
||||
self.update_status.emit(f"❌ Unexpected error during update: {e}")
|
||||
logger.exception(f"UpdateThread: Unexpected error during update: {e}")
|
||||
self.update_status.emit(_('update.unexpected_error', error=e))
|
||||
return False
|
||||
|
||||
def _update_via_pip(self) -> bool:
|
||||
"""Update yt-dlp via pip."""
|
||||
try:
|
||||
self.update_status.emit("🔍 Checking current pip installation...")
|
||||
self.update_status.emit(_('update.checking_pip'))
|
||||
self.update_progress.emit(30)
|
||||
|
||||
# Get current version
|
||||
try:
|
||||
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:
|
||||
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"
|
||||
|
||||
self.update_progress.emit(40)
|
||||
|
||||
# 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)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.update_status.emit("❌ Failed to check for updates")
|
||||
self.update_status.emit(_('update.failed_check_updates'))
|
||||
return False
|
||||
|
||||
data = response.json()
|
||||
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)
|
||||
|
||||
# Compare versions
|
||||
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)
|
||||
|
||||
try:
|
||||
# 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(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
|
||||
capture_output=True,
|
||||
@@ -260,33 +268,33 @@ class UpdateThread(QThread):
|
||||
self.update_progress.emit(85)
|
||||
|
||||
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)
|
||||
return True
|
||||
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
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.update_status.emit("❌ Pip update timed out after 5 minutes")
|
||||
self.update_status.emit(_("update.pip_timeout"))
|
||||
return False
|
||||
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
|
||||
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)
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
|
||||
class YTDLPUpdateDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Update yt-dlp")
|
||||
self.setWindowTitle(_('update.title'))
|
||||
self.setMinimumWidth(450)
|
||||
self.setMinimumHeight(200)
|
||||
self._closing = False # Flag to track if dialog is closing
|
||||
@@ -294,7 +302,7 @@ class YTDLPUpdateDialog(QDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 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.setWordWrap(True)
|
||||
self.status_label.setMinimumHeight(60)
|
||||
@@ -307,11 +315,11 @@ class YTDLPUpdateDialog(QDialog):
|
||||
|
||||
# Buttons
|
||||
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.setEnabled(False)
|
||||
|
||||
self.close_btn = QPushButton("Close")
|
||||
self.close_btn = QPushButton(_('buttons.close'))
|
||||
self.close_btn.clicked.connect(self.close)
|
||||
|
||||
button_layout.addWidget(self.update_btn)
|
||||
@@ -367,7 +375,7 @@ class YTDLPUpdateDialog(QDialog):
|
||||
self.check_version()
|
||||
|
||||
def check_version(self) -> None:
|
||||
self.status_label.setText("Checking for updates...")
|
||||
self.status_label.setText(_('update.checking'))
|
||||
self.update_btn.setEnabled(False)
|
||||
self.version_check_thread = VersionCheckThread()
|
||||
self.version_check_thread.finished.connect(self.on_version_check_finished)
|
||||
@@ -384,7 +392,7 @@ class YTDLPUpdateDialog(QDialog):
|
||||
return
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
@@ -395,32 +403,32 @@ class YTDLPUpdateDialog(QDialog):
|
||||
|
||||
if current_ver < latest_ver:
|
||||
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)
|
||||
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)
|
||||
except version.InvalidVersion:
|
||||
# If version parsing fails, do a simple string comparison
|
||||
if current_version != latest_version:
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
def perform_update(self) -> None:
|
||||
# Immediate visual feedback
|
||||
self.update_btn.setEnabled(False)
|
||||
self.close_btn.setEnabled(False)
|
||||
self.update_btn.setText("Updating...")
|
||||
self.status_label.setText("🚀 Initializing update process...")
|
||||
self.update_btn.setText(_('update.updating'))
|
||||
self.status_label.setText(_('update.initializing'))
|
||||
|
||||
# Show progress bar immediately
|
||||
self.progress_bar.setRange(0, 100)
|
||||
@@ -458,7 +466,7 @@ class YTDLPUpdateDialog(QDialog):
|
||||
self.progress_bar.setValue(100)
|
||||
self.status_label.setText(message)
|
||||
self.close_btn.setEnabled(True)
|
||||
self.update_btn.setText("Update") # Reset button text
|
||||
self.update_btn.setText(_('buttons.update')) # Reset button text
|
||||
|
||||
if success:
|
||||
# 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}")
|
||||
self.update_finished.emit(False, f"Network error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"AutoUpdateThread: Error during auto-update check: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
logger.exception(f"AutoUpdateThread: Error during auto-update check: {e}", exc_info=True)
|
||||
self.update_finished.emit(False, f"Update check error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
@@ -595,7 +600,7 @@ class AutoUpdateThread(QThread):
|
||||
return self._update_via_pip()
|
||||
|
||||
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
|
||||
|
||||
def _update_binary(self, yt_dlp_path: Path) -> bool:
|
||||
@@ -629,7 +634,7 @@ class AutoUpdateThread(QThread):
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
def _update_via_pip(self) -> bool:
|
||||
@@ -684,5 +689,5 @@ class AutoUpdateThread(QThread):
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
@@ -1,58 +1,99 @@
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
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 src.utils.ytsage_localization import _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.gui.ytsage_gui_main import YTSageApp
|
||||
|
||||
|
||||
class FormatSignals(QObject):
|
||||
format_update = Signal(list)
|
||||
|
||||
|
||||
class FormatTableMixin:
|
||||
def setup_format_table(self) -> QTableWidget:
|
||||
self.format_signals = FormatSignals()
|
||||
def _calculate_column_width(self, label: str, min_width: int, padding: int) -> int:
|
||||
"""Calculate responsive column width based on header text length."""
|
||||
self = cast("YTSageApp", self)
|
||||
font_metrics = QFontMetrics(self.format_table.horizontalHeader().font())
|
||||
text_width = font_metrics.horizontalAdvance(label)
|
||||
return max(text_width + padding, min_width)
|
||||
|
||||
def _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
|
||||
self.format_table = QTableWidget()
|
||||
self.format_table.setColumnCount(8)
|
||||
self.format_table.setHorizontalHeaderLabels(
|
||||
[
|
||||
"Select",
|
||||
"Quality",
|
||||
"Extension",
|
||||
"Resolution",
|
||||
"File Size",
|
||||
"Codec",
|
||||
"Audio",
|
||||
"Notes",
|
||||
]
|
||||
)
|
||||
self.format_table.setColumnCount(9)
|
||||
|
||||
# Get translated header labels
|
||||
header_labels = [
|
||||
_("formats.select"),
|
||||
_("formats.quality"),
|
||||
_("formats.extension"),
|
||||
_("formats.resolution"),
|
||||
_("formats.file_size"),
|
||||
_("formats.codec"),
|
||||
_("formats.audio"),
|
||||
_("formats.fps"),
|
||||
_("formats.hdr"),
|
||||
]
|
||||
self.format_table.setHorizontalHeaderLabels(header_labels)
|
||||
|
||||
# Enable alternating row colors
|
||||
self.format_table.setAlternatingRowColors(True)
|
||||
|
||||
# Set specific column widths and resize modes
|
||||
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # Select
|
||||
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)
|
||||
# Apply responsive column widths
|
||||
self._apply_column_widths(header_labels, is_playlist_mode=False)
|
||||
|
||||
# Set vertical header (row numbers) visible to false
|
||||
self.format_table.verticalHeader().setVisible(False)
|
||||
@@ -124,6 +165,8 @@ class FormatTableMixin:
|
||||
return self.format_table
|
||||
|
||||
def filter_formats(self) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
if not hasattr(self, "all_formats"):
|
||||
return
|
||||
|
||||
@@ -151,10 +194,13 @@ class FormatTableMixin:
|
||||
# Sort formats by quality
|
||||
def get_quality(f):
|
||||
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:
|
||||
res = resolution.split("x")[-1]
|
||||
return int(res)
|
||||
except ValueError:
|
||||
except (ValueError, IndexError):
|
||||
return 0
|
||||
else:
|
||||
return f.get("abr", 0)
|
||||
@@ -165,6 +211,8 @@ class FormatTableMixin:
|
||||
self.format_signals.format_update.emit(filtered_formats)
|
||||
|
||||
def _update_format_table(self, formats) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
self.format_table.setRowCount(0)
|
||||
self.format_checkboxes.clear()
|
||||
|
||||
@@ -172,64 +220,40 @@ class FormatTableMixin:
|
||||
|
||||
# Configure columns based on mode
|
||||
if is_playlist_mode:
|
||||
self.format_table.setColumnCount(5)
|
||||
self.format_table.setHorizontalHeaderLabels(["Select", "Quality", "Resolution", "Notes", "Audio"])
|
||||
self.format_table.setColumnCount(6)
|
||||
header_labels = [_("formats.select"), _("formats.quality"), _("formats.resolution"), _("formats.fps"), _("formats.hdr"), _("formats.audio")]
|
||||
self.format_table.setHorizontalHeaderLabels(header_labels)
|
||||
|
||||
# Configure column visibility and resizing for playlist mode
|
||||
self.format_table.setColumnHidden(5, True)
|
||||
self.format_table.setColumnHidden(6, True)
|
||||
self.format_table.setColumnHidden(7, True)
|
||||
self.format_table.setColumnHidden(8, True)
|
||||
|
||||
# Set specific resize modes for playlist columns
|
||||
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
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)
|
||||
# Apply responsive column widths for playlist mode
|
||||
self._apply_column_widths(header_labels, is_playlist_mode=True)
|
||||
|
||||
else:
|
||||
self.format_table.setColumnCount(8)
|
||||
self.format_table.setHorizontalHeaderLabels(
|
||||
[
|
||||
"Select",
|
||||
"Quality",
|
||||
"Extension",
|
||||
"Resolution",
|
||||
"File Size",
|
||||
"Codec",
|
||||
"Audio",
|
||||
"Notes",
|
||||
]
|
||||
)
|
||||
self.format_table.setColumnCount(9)
|
||||
header_labels = [
|
||||
_("formats.select"),
|
||||
_("formats.quality"),
|
||||
_("formats.extension"),
|
||||
_("formats.resolution"),
|
||||
_("formats.file_size"),
|
||||
_("formats.codec"),
|
||||
_("formats.audio"),
|
||||
_("formats.fps"),
|
||||
_("formats.hdr"),
|
||||
]
|
||||
self.format_table.setHorizontalHeaderLabels(header_labels)
|
||||
|
||||
# Ensure all columns are visible
|
||||
for i in range(2, 8):
|
||||
for i in range(2, 9):
|
||||
self.format_table.setColumnHidden(i, False)
|
||||
|
||||
# Reapply resize modes for non-playlist mode if needed (optional, might be okay without)
|
||||
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
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)
|
||||
# Apply responsive column widths for normal mode
|
||||
self._apply_column_widths(header_labels, is_playlist_mode=False)
|
||||
|
||||
# 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:
|
||||
row = self.format_table.rowCount()
|
||||
@@ -252,14 +276,15 @@ class FormatTableMixin:
|
||||
# Column 1: Quality (Always shown)
|
||||
quality_text = self.get_quality_label(f)
|
||||
quality_item = QTableWidgetItem(quality_text)
|
||||
# Set color based on quality
|
||||
if "Best" in quality_text:
|
||||
# Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms)
|
||||
quality_lower = quality_text.lower() # Make comparison case-insensitive
|
||||
if any(term.lower() in quality_lower for term in ["Best", "Óptima", "Mejor", "Melhor", "Лучшее", "最佳", "Beste", "Meilleure", "सर्वोत्तम", "Terbaik", "En iyi", "Najlepsza", "Najlepszy", "Najlepsze", "Migliore", "Miglior", "الأفضل", "أفضل", "最高"]):
|
||||
quality_item.setForeground(QColor("#00ff00")) # Green for best quality
|
||||
elif "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
|
||||
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
|
||||
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
|
||||
self.format_table.setItem(row, 1, quality_item)
|
||||
|
||||
@@ -268,37 +293,67 @@ class FormatTableMixin:
|
||||
# Column 2: Resolution (Always shown)
|
||||
resolution = f.get("resolution", "N/A")
|
||||
if f.get("vcodec") == "none":
|
||||
resolution = "Audio only"
|
||||
resolution = _("formats.audio_only_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:
|
||||
# Get notes for playlist mode
|
||||
notes = self._get_format_notes(f)
|
||||
notes_item = QTableWidgetItem(notes)
|
||||
if "✨ Recommended" in notes:
|
||||
notes_item.setForeground(QColor("#00ff00")) # Green for recommended
|
||||
elif "💾 Storage friendly" in notes:
|
||||
notes_item.setForeground(QColor("#00ccff")) # Blue for storage friendly
|
||||
elif "📱 Mobile friendly" in notes:
|
||||
notes_item.setForeground(QColor("#ff9900")) # Orange for mobile
|
||||
self.format_table.setItem(row, 3, notes_item)
|
||||
# Get FPS for playlist mode
|
||||
fps_value = f.get("fps")
|
||||
if fps_value is not None:
|
||||
# Format FPS value appropriately
|
||||
if fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A" # Very low fps like storyboards
|
||||
else:
|
||||
fps_text = "N/A"
|
||||
|
||||
fps_item = QTableWidgetItem(fps_text)
|
||||
# Color code based on FPS value
|
||||
if fps_value and fps_value >= 60:
|
||||
fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps
|
||||
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:
|
||||
# Extension for normal mode (column 2)
|
||||
self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper()))
|
||||
|
||||
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status
|
||||
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge
|
||||
audio_status = "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)
|
||||
if needs_audio:
|
||||
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
|
||||
else: # Has Audio (Video+Audio)
|
||||
audio_item.setForeground(QColor("#00cc00")) # Green for included audio
|
||||
# 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)
|
||||
|
||||
# --- Populate columns only shown in non-playlist mode ---
|
||||
@@ -319,45 +374,83 @@ class FormatTableMixin:
|
||||
codec += f" / {f.get('acodec', 'N/A')}"
|
||||
self.format_table.setItem(row, 5, QTableWidgetItem(codec))
|
||||
|
||||
# Column 7: Notes
|
||||
notes = self._get_format_notes(f)
|
||||
notes_item = QTableWidgetItem(notes)
|
||||
if "✨ Recommended" in notes:
|
||||
notes_item.setForeground(QColor("#00ff00")) # Green for recommended
|
||||
elif "💾 Storage friendly" in notes:
|
||||
notes_item.setForeground(QColor("#00ccff")) # Blue for storage friendly
|
||||
elif "📱 Mobile friendly" in notes:
|
||||
notes_item.setForeground(QColor("#ff9900")) # Orange for mobile
|
||||
self.format_table.setItem(row, 7, notes_item)
|
||||
# Column 7: FPS (Frame Rate)
|
||||
fps_value = f.get("fps")
|
||||
if fps_value is not None:
|
||||
# Format FPS value appropriately
|
||||
if fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A" # Very low fps like storyboards
|
||||
else:
|
||||
fps_text = "N/A"
|
||||
|
||||
fps_item = QTableWidgetItem(fps_text)
|
||||
# Color code based on FPS value
|
||||
if fps_value and fps_value >= 60:
|
||||
fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps
|
||||
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:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
for checkbox in self.format_checkboxes:
|
||||
if checkbox != clicked_checkbox:
|
||||
checkbox.setChecked(False)
|
||||
|
||||
def get_selected_format(self):
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
for checkbox in self.format_checkboxes:
|
||||
if checkbox.isChecked():
|
||||
return checkbox.format_id
|
||||
return None
|
||||
|
||||
def update_format_table(self, formats) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
self.all_formats = formats
|
||||
self.format_signals.format_update.emit(formats)
|
||||
|
||||
def get_quality_label(self, format_info) -> str:
|
||||
"""Determine quality label based on format information"""
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
if format_info.get("vcodec") == "none":
|
||||
# Audio quality
|
||||
abr = format_info.get("abr", 0)
|
||||
if abr >= 256:
|
||||
return "Best Audio"
|
||||
return _("formats.best_audio")
|
||||
elif abr >= 192:
|
||||
return "High Audio"
|
||||
return _("formats.high_audio")
|
||||
elif abr >= 128:
|
||||
return "Medium Audio"
|
||||
return _("formats.medium_audio")
|
||||
else:
|
||||
return "Low Audio"
|
||||
return _("formats.low_audio")
|
||||
else:
|
||||
# Video quality
|
||||
height = 0
|
||||
@@ -369,55 +462,16 @@ class FormatTableMixin:
|
||||
pass
|
||||
|
||||
if height >= 2160:
|
||||
return "Best (4K)"
|
||||
return _("formats.best_4k")
|
||||
elif height >= 1440:
|
||||
return "Best (2K)"
|
||||
return _("formats.best_2k")
|
||||
elif height >= 1080:
|
||||
return "High (1080p)"
|
||||
return _("formats.high_1080p")
|
||||
elif height >= 720:
|
||||
return "High (720p)"
|
||||
return _("formats.high_720p")
|
||||
elif height >= 480:
|
||||
return "Medium (480p)"
|
||||
return _("formats.medium_480p")
|
||||
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)
|
||||
|
||||
@@ -2,22 +2,29 @@ import re
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
from PySide6.QtCore import Qt
|
||||
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
|
||||
SponsorBlockCategoryDialog,
|
||||
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:
|
||||
def setup_video_info_section(self) -> QHBoxLayout:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
# Create a horizontal layout for thumbnail and video info
|
||||
media_info_layout = QHBoxLayout()
|
||||
media_info_layout.setSpacing(15)
|
||||
@@ -89,7 +96,7 @@ class VideoInfoMixin:
|
||||
subtitle_layout.setSpacing(10)
|
||||
|
||||
# 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.setFixedWidth(150) # Let it size naturally or adjust as needed
|
||||
self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog)
|
||||
@@ -118,7 +125,7 @@ class VideoInfoMixin:
|
||||
subtitle_layout.addWidget(self.subtitle_select_btn)
|
||||
|
||||
# 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;")
|
||||
subtitle_layout.addWidget(self.selected_subs_label)
|
||||
|
||||
@@ -132,7 +139,7 @@ class VideoInfoMixin:
|
||||
# --- SponsorBlock Section ---
|
||||
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.clicked.connect(self.open_sponsorblock_dialog)
|
||||
self.sponsorblock_select_btn.setStyleSheet(
|
||||
@@ -161,7 +168,7 @@ class VideoInfoMixin:
|
||||
sponsorblock_layout.addWidget(self.sponsorblock_select_btn)
|
||||
|
||||
# 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;")
|
||||
sponsorblock_layout.addWidget(self.selected_sponsorblock_label)
|
||||
|
||||
@@ -184,6 +191,8 @@ class VideoInfoMixin:
|
||||
return media_info_layout
|
||||
|
||||
def setup_playlist_info_section(self) -> QLabel:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
self.playlist_info_label = QLabel()
|
||||
self.playlist_info_label.setVisible(False)
|
||||
self.playlist_info_label.setStyleSheet(
|
||||
@@ -205,12 +214,14 @@ class VideoInfoMixin:
|
||||
return self.playlist_info_label
|
||||
|
||||
def update_video_info(self, info) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
if hasattr(self, "is_playlist") and self.is_playlist:
|
||||
# Playlist Mode: Show playlist title and video count
|
||||
self.title_label.setText(self.playlist_info.get("title", "Unknown Playlist"))
|
||||
self.title_label.setText(self.playlist_info.get("title", _("playlist.unknown")))
|
||||
|
||||
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
|
||||
self.channel_label.setText("")
|
||||
@@ -243,7 +254,7 @@ class VideoInfoMixin:
|
||||
date_obj = datetime.strptime(upload_date, "%Y%m%d")
|
||||
formatted_date = date_obj.strftime("%B %d, %Y")
|
||||
else:
|
||||
formatted_date = "Unknown date"
|
||||
formatted_date = _("video_info.unknown_date")
|
||||
|
||||
# Format duration
|
||||
duration = info.get("duration", 0)
|
||||
@@ -251,15 +262,17 @@ class VideoInfoMixin:
|
||||
seconds = duration % 60
|
||||
duration_str = f"{minutes}:{seconds:02d}"
|
||||
|
||||
# Update labels
|
||||
self.title_label.setText(info.get("title", "Unknown title"))
|
||||
self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}")
|
||||
self.views_label.setText(f"Views: {formatted_views}")
|
||||
self.like_count_label.setText(f"Likes: {formatted_likes}")
|
||||
self.date_label.setText(f"Upload date: {formatted_date}")
|
||||
self.duration_label.setText(f"Duration: {duration_str}")
|
||||
# Update labels with localized text
|
||||
self.title_label.setText(info.get("title", _("video_info.unknown_title")))
|
||||
self.channel_label.setText(f"{_("video_info.channel")}: {info.get('uploader', _("video_info.unknown_channel"))}")
|
||||
self.views_label.setText(f"{_("video_info.views")}: {formatted_views}")
|
||||
self.like_count_label.setText(f"{_("video_info.likes")}: {formatted_likes}")
|
||||
self.date_label.setText(f"{_("video_info.upload_date")}: {formatted_date}")
|
||||
self.duration_label.setText(f"{_("video_info.duration")}: {duration_str}")
|
||||
|
||||
def open_subtitle_dialog(self) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
if not hasattr(self, "available_subtitles") or not hasattr(self, "available_automatic_subtitles"):
|
||||
logger.warning("Subtitle info not loaded yet.")
|
||||
return
|
||||
@@ -274,29 +287,21 @@ class VideoInfoMixin:
|
||||
self, # Parent for the dialog
|
||||
)
|
||||
|
||||
# Access the main application window (parent of the mixin's widget)
|
||||
# to find the merge checkbox
|
||||
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)
|
||||
# removed extra logic for mapping to main_windows
|
||||
merge_checkbox = getattr(self, "merge_subs_checkbox", None)
|
||||
|
||||
if dialog.exec(): # If user clicks OK
|
||||
self.selected_subtitles = dialog.get_selected_subtitles()
|
||||
logger.info(f"Selected subtitles: {self.selected_subtitles}")
|
||||
# Update UI to reflect selection
|
||||
count = len(self.selected_subtitles)
|
||||
self.selected_subs_label.setText(f"{count} selected")
|
||||
self.selected_subs_label.setText(_("subtitle_selection.count_selected", count=count))
|
||||
self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0)
|
||||
|
||||
# Enable/disable the merge checkbox in the parent window
|
||||
if merge_checkbox:
|
||||
# Only enable merge checkbox if we're not in Audio Only mode
|
||||
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
|
||||
should_enable = count > 0 and not is_audio_only
|
||||
merge_checkbox.setEnabled(should_enable)
|
||||
@@ -310,6 +315,8 @@ class VideoInfoMixin:
|
||||
|
||||
def open_sponsorblock_dialog(self) -> None:
|
||||
"""Open the SponsorBlock category selection dialog."""
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
# Initialize selected categories if not exists or empty (first time opening)
|
||||
if not hasattr(self, "selected_sponsorblock_categories") or not self.selected_sponsorblock_categories:
|
||||
# Use None to let the dialog set its own defaults
|
||||
@@ -326,6 +333,8 @@ class VideoInfoMixin:
|
||||
|
||||
def _update_sponsorblock_display(self) -> None:
|
||||
"""Update the SponsorBlock button and label to reflect current selection."""
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
if not hasattr(self, "selected_sponsorblock_categories"):
|
||||
self.selected_sponsorblock_categories = []
|
||||
|
||||
@@ -333,11 +342,11 @@ class VideoInfoMixin:
|
||||
|
||||
# Update label text
|
||||
if count == 0:
|
||||
self.selected_sponsorblock_label.setText("0 selected")
|
||||
self.selected_sponsorblock_label.setText(_("selection.none_selected"))
|
||||
elif count == 1:
|
||||
self.selected_sponsorblock_label.setText("1 category selected")
|
||||
self.selected_sponsorblock_label.setText(_("selection.one_selected"))
|
||||
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
|
||||
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0)
|
||||
@@ -347,6 +356,8 @@ class VideoInfoMixin:
|
||||
self.sponsorblock_select_btn.style().polish(self.sponsorblock_select_btn)
|
||||
|
||||
def download_thumbnail(self, url) -> None:
|
||||
self = cast("YTSageApp", self) # for autocompletion and type inference.
|
||||
|
||||
try:
|
||||
# Store both thumbnail URL and video URL
|
||||
self.thumbnail_url = url
|
||||
@@ -364,7 +375,10 @@ class VideoInfoMixin:
|
||||
pixmap.loadFromData(img_byte_arr.getvalue())
|
||||
self.thumbnail_label.setPixmap(pixmap)
|
||||
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:
|
||||
if not self.save_thumbnail:
|
||||
@@ -373,10 +387,11 @@ class VideoInfoMixin:
|
||||
try:
|
||||
# Import yt_dlp locally to avoid import errors when yt-dlp is not installed
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
|
||||
logger.debug(f"Attempting to save thumbnail for URL: {video_url}")
|
||||
|
||||
ydl_opts = {
|
||||
"logger": logger,
|
||||
"quiet": True,
|
||||
"skip_download": True,
|
||||
"force_generic_extractor": False,
|
||||
@@ -389,7 +404,7 @@ class VideoInfoMixin:
|
||||
thumbnails = info.get("thumbnails", [])
|
||||
|
||||
if not thumbnails:
|
||||
raise ValueError("No thumbnails available")
|
||||
logger.info("No thumbnails available")
|
||||
|
||||
thumbnail_url = max(
|
||||
thumbnails,
|
||||
@@ -397,7 +412,7 @@ class VideoInfoMixin:
|
||||
).get("url")
|
||||
|
||||
if not thumbnail_url:
|
||||
raise ValueError("Failed to extract thumbnail URL")
|
||||
logger.info("Failed to extract thumbnail URL")
|
||||
|
||||
# Download using requests
|
||||
response = requests.get(thumbnail_url)
|
||||
@@ -418,8 +433,8 @@ class VideoInfoMixin:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"❌ Thumbnail error: {str(e)}"
|
||||
logger.error(f"Thumbnail Save Error: {str(e)}")
|
||||
error_msg = f"❌ Thumbnail error: {e}"
|
||||
logger.exception(f"Thumbnail Save Error: {e}")
|
||||
self.signals.update_status.emit(error_msg)
|
||||
return False
|
||||
|
||||
|
||||
@@ -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.")
|
||||
@@ -30,6 +30,29 @@ def get_asset_path(asset_relative_path: str) -> Path:
|
||||
Returns:
|
||||
Path: Absolute path to the asset file
|
||||
"""
|
||||
# Check if running as a frozen executable (PyInstaller, cx_Freeze, etc.)
|
||||
if getattr(sys, "frozen", False):
|
||||
# Running as a frozen executable
|
||||
# Try sys._MEIPASS first (PyInstaller)
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
asset_path = Path(sys._MEIPASS) / asset_relative_path
|
||||
if asset_path.exists():
|
||||
return asset_path
|
||||
|
||||
# For cx_Freeze, assets are typically in lib/ directory next to the executable
|
||||
executable_dir = Path(sys.executable).parent
|
||||
|
||||
# Try with lib/ prefix (cx_Freeze standard structure)
|
||||
asset_path = executable_dir / "lib" / asset_relative_path
|
||||
if asset_path.exists():
|
||||
return asset_path
|
||||
|
||||
# Try directly in executable directory
|
||||
asset_path = executable_dir / asset_relative_path
|
||||
if asset_path.exists():
|
||||
return asset_path
|
||||
|
||||
# Not frozen - try importlib.resources for installed packages
|
||||
try:
|
||||
# Use importlib.resources (standard in Python 3.9+)
|
||||
import importlib.resources as resources
|
||||
@@ -59,13 +82,15 @@ SOUND_PATH: Path = get_asset_path("assets/sound/notification.mp3")
|
||||
|
||||
OS_NAME: str = platform.system() # Windows ; Darwin ; Linux
|
||||
|
||||
IS_FROZEN = getattr(sys, "frozen", False)
|
||||
USER_HOME_DIR: Path = Path.home()
|
||||
|
||||
# OS Specific Constants
|
||||
if OS_NAME == "Windows":
|
||||
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
|
||||
|
||||
# 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_BIN_DIR: Path = APP_DIR / "bin"
|
||||
APP_DATA_DIR: Path = APP_DIR / "data"
|
||||
@@ -74,9 +99,6 @@ if OS_NAME == "Windows":
|
||||
|
||||
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"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -84,6 +106,8 @@ elif OS_NAME == "Darwin": # macOS
|
||||
_mac_version = platform.mac_ver()[0]
|
||||
OS_FULL_NAME: str = f"macOS {_mac_version}" if _mac_version else "macOS"
|
||||
|
||||
# Always use user data directory for app data, logs, config, and binaries
|
||||
# Even when frozen, we don't want to create these folders next to the executable
|
||||
APP_DIR: Path = USER_HOME_DIR / "Library" / "Application Support" / "YTSage"
|
||||
APP_BIN_DIR: Path = APP_DIR / "bin"
|
||||
APP_DATA_DIR: Path = APP_DIR / "data"
|
||||
@@ -92,9 +116,6 @@ elif OS_NAME == "Darwin": # 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"
|
||||
|
||||
# Documentation URLs
|
||||
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
|
||||
|
||||
SUBPROCESS_CREATIONFLAGS: int = 0
|
||||
|
||||
@@ -102,6 +123,8 @@ elif OS_NAME == "Darwin": # macOS
|
||||
else: # Linux and other UNIX-like
|
||||
OS_FULL_NAME: str = f"{OS_NAME} {platform.release()}"
|
||||
|
||||
# Always use user data directory for app data, logs, config, and binaries
|
||||
# Even when frozen, we don't want to create these folders next to the executable
|
||||
APP_DIR: Path = USER_HOME_DIR / ".local" / "share" / "YTSage"
|
||||
APP_BIN_DIR: Path = APP_DIR / "bin"
|
||||
APP_DATA_DIR: Path = APP_DIR / "data"
|
||||
@@ -110,12 +133,11 @@ else: # Linux and other UNIX-like
|
||||
|
||||
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"
|
||||
|
||||
# Documentation URLs
|
||||
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options"
|
||||
|
||||
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_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 this file is run directly, print directory information; if imported, create the necessary directories for the application.
|
||||
# for debug, to check os specific variable which can be different based on os.
|
||||
info = {
|
||||
"OS_NAME": OS_NAME,
|
||||
"OS_FULL_NAME": OS_FULL_NAME,
|
||||
@@ -135,7 +158,6 @@ if __name__ == "__main__":
|
||||
"APP_CONFIG_FILE": str(APP_CONFIG_FILE),
|
||||
"YTDLP_DOWNLOAD_URL": YTDLP_DOWNLOAD_URL,
|
||||
"YTDLP_APP_BIN_PATH": YTDLP_APP_BIN_PATH,
|
||||
"YTDLP_DOCS_URL": YTDLP_DOCS_URL,
|
||||
"SUBPROCESS_CREATIONFLAGS": SUBPROCESS_CREATIONFLAGS,
|
||||
}
|
||||
for key, value in info.items():
|
||||
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||