Compare commits
10 Commits
48ba590adb
...
main
Author | SHA1 | Date | |
---|---|---|---|
2fcd202532 | |||
cd8476b506 | |||
fdb44d4011 | |||
1eb761ec43 | |||
4eb71e74fc | |||
af57cee939 | |||
60194da877 | |||
![]() |
018ddb1a0f | ||
![]() |
e9d3b9e43f | ||
![]() |
b9041445ba |
90
README.md
90
README.md
@@ -1,9 +1,10 @@
|
||||
# YouTube Channel Archiver
|
||||
|
||||
A cross-platform Python script for downloading and archiving entire YouTube channels using `yt-dlp`. Perfect for content preservation, offline viewing, and creating personal archives of your favorite channels.
|
||||
A cross-platform Python application for downloading and archiving entire YouTube channels using `yt-dlp`. Features both a graphical user interface (GUI) and command-line interface (CLI) for maximum flexibility. Perfect for content preservation, offline viewing, and creating personal archives of your favorite channels.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🖥️ **Dual Interface**: Easy-to-use GUI and powerful command-line interface
|
||||
- 🌍 **Cross-platform**: Works on macOS, Linux, and Windows
|
||||
- 📦 **Automatic setup**: Installs `yt-dlp` dependency automatically if needed
|
||||
- 📁 **Smart organization**: Creates channel-specific folders with date-organized files
|
||||
@@ -12,36 +13,45 @@ A cross-platform Python script for downloading and archiving entire YouTube chan
|
||||
- 📝 **Metadata preservation**: Downloads video descriptions, info, and thumbnails
|
||||
- ⚡ **Error resilient**: Continues downloading even if individual videos fail
|
||||
- 🎯 **Flexible input**: Supports various YouTube URL formats and channel IDs
|
||||
- 🛑 **Stoppable downloads**: Cancel downloads anytime with proper cleanup
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### GUI Version (Recommended for beginners)
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <your-gitea-repo-url>
|
||||
git clone https://git.ewnix.net/phlux/yt-channel-archiver
|
||||
cd youtube-channel-archiver
|
||||
|
||||
# Run the script (it will install yt-dlp automatically if needed)
|
||||
# Run the GUI (it will install yt-dlp automatically if needed)
|
||||
python youtube_archiver_gui.py
|
||||
```
|
||||
|
||||
### Command Line Version
|
||||
```bash
|
||||
# Run the CLI script directly
|
||||
python youtube_archiver.py "https://www.youtube.com/@channelname"
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **Python 3.6+**
|
||||
- **tkinter** (usually included with Python - needed for GUI only)
|
||||
- **pip** (for automatic yt-dlp installation)
|
||||
- Internet connection
|
||||
|
||||
The script will automatically install `yt-dlp` if it's not already available on your system.
|
||||
The application will automatically install `yt-dlp` if it's not already available on your system.
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
### Option 1: Direct Download
|
||||
Download `youtube_archiver.py` and run it directly - no additional setup required!
|
||||
Download both `youtube_archiver_gui.py` and `youtube_archiver.py` and run directly - no additional setup required!
|
||||
|
||||
### Option 2: Clone Repository
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
git clone https://git.ewnix.net/phlux/yt-channel-archiver.git
|
||||
cd youtube-channel-archiver
|
||||
chmod +x youtube_archiver.py # On Unix systems
|
||||
chmod +x *.py # On Unix systems
|
||||
```
|
||||
|
||||
### Option 3: Manual yt-dlp Installation
|
||||
@@ -52,19 +62,45 @@ pip install yt-dlp
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### Basic Usage
|
||||
### GUI Interface
|
||||
|
||||
1. **Launch the GUI:**
|
||||
```bash
|
||||
python youtube_archiver_gui.py
|
||||
```
|
||||
|
||||
2. **Fill in the details:**
|
||||
- Enter the YouTube channel URL
|
||||
- Choose an output directory (or use default)
|
||||
- Adjust quality and options as needed
|
||||
|
||||
3. **Start downloading:**
|
||||
- Click "Download Channel"
|
||||
- Monitor progress in real-time
|
||||
- Click "Stop Download" to cancel if needed
|
||||
|
||||
#### GUI Features:
|
||||
- **Live Progress**: Real-time output and progress indication
|
||||
- **Quality Selection**: Dropdown menu for video quality
|
||||
- **Options**: Checkboxes for audio-only, thumbnails, and metadata
|
||||
- **Directory Browser**: Easy folder selection
|
||||
- **Status Updates**: Clear feedback on download status
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
#### Basic Usage
|
||||
```bash
|
||||
python youtube_archiver.py "CHANNEL_URL"
|
||||
```
|
||||
|
||||
### Supported URL Formats
|
||||
#### Supported URL Formats
|
||||
- `https://www.youtube.com/@channelname`
|
||||
- `https://www.youtube.com/c/channelname`
|
||||
- `https://www.youtube.com/user/username`
|
||||
- `https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxx`
|
||||
- `UCxxxxxxxxxxxxxxxxxxx` (Channel ID only)
|
||||
|
||||
### Command Line Options
|
||||
#### Command Line Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
@@ -75,7 +111,7 @@ python youtube_archiver.py "CHANNEL_URL"
|
||||
| `--no-metadata` | Skip metadata files | False |
|
||||
| `--help`, `-h` | Show help message | - |
|
||||
|
||||
### Examples
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Download all videos in best quality
|
||||
@@ -93,7 +129,7 @@ python youtube_archiver.py "https://www.youtube.com/@newschannel" --no-thumbnail
|
||||
|
||||
## 📁 Output Structure
|
||||
|
||||
The script creates an organized directory structure:
|
||||
Both interfaces create the same organized directory structure:
|
||||
|
||||
```
|
||||
Channel_Name/
|
||||
@@ -115,12 +151,22 @@ Channel_Name/
|
||||
|
||||
## 🔄 Resuming Downloads
|
||||
|
||||
The script automatically tracks downloaded videos in `download_archive.txt`. If a download is interrupted:
|
||||
Both interfaces automatically track downloaded videos in `download_archive.txt`. If a download is interrupted:
|
||||
|
||||
1. Simply run the same command again
|
||||
1. Simply restart the application/command
|
||||
2. Already downloaded videos will be skipped
|
||||
3. New or failed videos will be downloaded
|
||||
|
||||
## 🖼️ GUI Screenshots
|
||||
|
||||
The GUI provides an intuitive interface with:
|
||||
- **Channel URL Input**: Easy copy-paste of YouTube URLs
|
||||
- **Directory Selection**: Browse button for choosing download location
|
||||
- **Quality Options**: Dropdown for video quality selection
|
||||
- **Download Options**: Checkboxes for audio-only, thumbnails, and metadata
|
||||
- **Progress Monitoring**: Real-time download progress and output
|
||||
- **Status Bar**: Current operation status
|
||||
|
||||
## ⚠ Legal Considerations
|
||||
|
||||
- **Respect copyright**: Only download content you have permission to archive
|
||||
@@ -132,16 +178,20 @@ The script automatically tracks downloaded videos in `download_archive.txt`. If
|
||||
|
||||
### Common Issues
|
||||
|
||||
**GUI won't start**
|
||||
- Ensure Python includes tkinter: `python -c "import tkinter"`
|
||||
- On some Linux distributions: `sudo apt-get install python3-tk`
|
||||
|
||||
**"yt-dlp not found" error**
|
||||
- The script should install it automatically
|
||||
- Both interfaces should install it automatically
|
||||
- If not, manually install: `pip install yt-dlp`
|
||||
|
||||
**Permission denied errors**
|
||||
- On Unix systems: `chmod +x youtube_archiver.py`
|
||||
- On Unix systems: `chmod +x *.py`
|
||||
- Run with appropriate permissions
|
||||
|
||||
**Network/download errors**
|
||||
- The script continues on errors - check the output for specific failures
|
||||
- The application continues on errors - check the output for specific failures
|
||||
- Some videos may be unavailable due to geographic restrictions or privacy settings
|
||||
|
||||
**Python not found**
|
||||
@@ -151,10 +201,10 @@ The script automatically tracks downloaded videos in `download_archive.txt`. If
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
1. Check the console output for specific error messages
|
||||
1. Check the console/GUI output for specific error messages
|
||||
2. Ensure you have a stable internet connection
|
||||
3. Verify the channel URL is correct and publicly accessible
|
||||
4. Try running with `--quality worst` to test with smaller files
|
||||
4. Try running with lower quality settings to test with smaller files
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -163,6 +213,7 @@ Contributions are welcome! Please feel free to:
|
||||
- Suggest features
|
||||
- Submit pull requests
|
||||
- Improve documentation
|
||||
- Add new GUI features
|
||||
|
||||
## 📄 License
|
||||
|
||||
@@ -171,6 +222,7 @@ This project is provided as-is for educational and personal archiving purposes.
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Built using [yt-dlp](https://github.com/yt-dlp/yt-dlp) - the excellent YouTube downloading library
|
||||
- GUI built with Python's built-in tkinter library for maximum compatibility
|
||||
- Inspired by the need for content preservation and offline access
|
||||
|
||||
---
|
||||
|
21
windows/LICENSE.txt
Normal file
21
windows/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 YouTube Channel Archiver
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
48
windows/README.txt
Normal file
48
windows/README.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
YouTube Channel Archiver v1.0
|
||||
==============================
|
||||
|
||||
A simple, user-friendly tool to download entire YouTube channels.
|
||||
|
||||
FEATURES:
|
||||
✓ Download all videos from any YouTube channel
|
||||
✓ Choose video quality (720p, 1080p, best quality, etc.)
|
||||
✓ Audio-only downloads supported
|
||||
✓ Download thumbnails and metadata
|
||||
✓ Resume interrupted downloads automatically
|
||||
✓ Automatic dependency management
|
||||
✓ Clean, intuitive interface
|
||||
|
||||
SYSTEM REQUIREMENTS:
|
||||
- Windows 10 or later (64-bit)
|
||||
- Internet connection for downloads
|
||||
- Sufficient disk space for videos
|
||||
|
||||
QUICK START:
|
||||
1. Launch YouTube Channel Archiver
|
||||
2. Paste any YouTube channel URL
|
||||
3. Choose your download preferences
|
||||
4. Select where to save the videos
|
||||
5. Click "Download Channel"
|
||||
|
||||
SUPPORTED URL FORMATS:
|
||||
- https://www.youtube.com/@channelname
|
||||
- https://www.youtube.com/c/channelname
|
||||
- https://www.youtube.com/user/username
|
||||
- https://www.youtube.com/channel/UCxxxxxxxxx
|
||||
|
||||
The application will automatically:
|
||||
- Install required dependencies (yt-dlp)
|
||||
- Create organized folders by channel name
|
||||
- Track downloaded videos to avoid duplicates
|
||||
- Handle errors gracefully and continue downloading
|
||||
|
||||
TROUBLESHOOTING:
|
||||
- Ensure you have a stable internet connection
|
||||
- Check that the YouTube URL is correct and accessible
|
||||
- Make sure you have enough disk space
|
||||
- Some videos may be unavailable due to region restrictions
|
||||
|
||||
For support and updates, visit: https://github.com/yt-dlp/yt-dlp
|
||||
|
||||
LICENSE:
|
||||
This software is provided as-is under the MIT License.
|
148
windows/build_windows.bat
Normal file
148
windows/build_windows.bat
Normal file
@@ -0,0 +1,148 @@
|
||||
@echo off
|
||||
title YouTube Channel Archiver - Windows Build Script
|
||||
echo ================================================================
|
||||
echo YouTube Channel Archiver - Windows Build Script
|
||||
echo ================================================================
|
||||
echo.
|
||||
|
||||
echo [STEP 1] Checking Python installation...
|
||||
python --version > nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Python is not installed or not in PATH
|
||||
echo.
|
||||
echo Please install Python from https://python.org
|
||||
echo Make sure to check "Add Python to PATH" during installation
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
for /f "tokens=2" %%i in ('python --version 2^>^&1') do set PYTHON_VERSION=%%i
|
||||
echo SUCCESS: Python %PYTHON_VERSION% is installed
|
||||
|
||||
echo.
|
||||
echo [STEP 2] Checking PyInstaller installation...
|
||||
python -c "import PyInstaller" > nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo PyInstaller not found. Installing now...
|
||||
echo.
|
||||
echo [STEP 2a] Updating pip...
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
echo.
|
||||
echo [STEP 2b] Installing PyInstaller...
|
||||
python -m pip install pyinstaller
|
||||
|
||||
echo.
|
||||
echo [STEP 2c] Verifying PyInstaller installation...
|
||||
python -c "import PyInstaller; print('PyInstaller version:', PyInstaller.__version__)"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: PyInstaller installation failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo SUCCESS: PyInstaller installed successfully
|
||||
) else (
|
||||
echo SUCCESS: PyInstaller is already installed
|
||||
python -c "import PyInstaller; print('PyInstaller version:', PyInstaller.__version__)"
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [STEP 3] Checking for source file...
|
||||
if not exist "youtube_archiver_standalone.py" (
|
||||
echo ERROR: youtube_archiver_standalone.py not found in current directory
|
||||
echo Current directory: %CD%
|
||||
echo.
|
||||
echo Files in current directory:
|
||||
dir *.py
|
||||
echo.
|
||||
echo Please make sure the Python script is in the same folder as this batch file
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [STEP 4] Building executable with PyInstaller...
|
||||
echo Command: python -m PyInstaller --onefile --windowed --name "YouTubeChannelArchiver" --distpath dist --workpath build --clean youtube_archiver_standalone.py
|
||||
echo.
|
||||
|
||||
REM Use python -m PyInstaller instead of direct pyinstaller command
|
||||
python -m PyInstaller --onefile --windowed --name "YouTubeChannelArchiver" --distpath dist --workpath build --clean youtube_archiver_standalone.py
|
||||
|
||||
echo.
|
||||
echo [STEP 5] Verifying build...
|
||||
if exist "dist\YouTubeChannelArchiver.exe" (
|
||||
echo SUCCESS: Executable created successfully!
|
||||
echo Location: %CD%\dist\YouTubeChannelArchiver.exe
|
||||
|
||||
REM Get file size
|
||||
for %%A in ("dist\YouTubeChannelArchiver.exe") do (
|
||||
set "filesize=%%~zA"
|
||||
)
|
||||
echo File size: %filesize% bytes
|
||||
|
||||
echo.
|
||||
echo [STEP 6] Creating portable package...
|
||||
if not exist "portable" mkdir portable
|
||||
copy "dist\YouTubeChannelArchiver.exe" "portable\" > nul
|
||||
if exist "README.txt" copy "README.txt" "portable\" > nul
|
||||
if exist "LICENSE.txt" copy "LICENSE.txt" "portable\" > nul
|
||||
echo Portable version created in 'portable' folder
|
||||
|
||||
) else (
|
||||
echo ERROR: Executable was not created
|
||||
echo.
|
||||
echo Checking for build errors in the output above...
|
||||
echo Common issues:
|
||||
echo - Missing dependencies
|
||||
echo - Antivirus blocking the build
|
||||
echo - Insufficient disk space
|
||||
echo - Path issues with special characters
|
||||
echo.
|
||||
echo Build files location:
|
||||
if exist "build" (
|
||||
echo Build directory exists: %CD%\build
|
||||
) else (
|
||||
echo Build directory not found
|
||||
)
|
||||
|
||||
if exist "dist" (
|
||||
echo Dist directory exists: %CD%\dist
|
||||
echo Contents of dist directory:
|
||||
dir dist
|
||||
) else (
|
||||
echo Dist directory not found
|
||||
)
|
||||
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ================================================================
|
||||
echo BUILD COMPLETED SUCCESSFULLY!
|
||||
echo ================================================================
|
||||
echo.
|
||||
echo Files created:
|
||||
echo - dist\YouTubeChannelArchiver.exe (main executable - %filesize% bytes)
|
||||
echo - portable\YouTubeChannelArchiver.exe (portable version)
|
||||
echo.
|
||||
echo Architecture info:
|
||||
python -c "import platform; print('Python architecture:', platform.architecture()[0])"
|
||||
python -c "import platform; print('Machine type:', platform.machine())"
|
||||
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Test the executable: dist\YouTubeChannelArchiver.exe
|
||||
echo 2. Distribute the portable folder, or
|
||||
echo 3. Create an installer using installer.iss with Inno Setup
|
||||
echo.
|
||||
echo Press any key to open the dist folder...
|
||||
pause > nul
|
||||
|
||||
REM Try to open the folder
|
||||
if exist "dist\YouTubeChannelArchiver.exe" (
|
||||
explorer dist
|
||||
) else (
|
||||
echo Could not open folder automatically
|
||||
)
|
38
windows/installer.iss
Normal file
38
windows/installer.iss
Normal file
@@ -0,0 +1,38 @@
|
||||
[Setup]
|
||||
AppName=YouTube Channel Archiver
|
||||
AppVersion=1.0.0
|
||||
AppPublisher=YouTube Channel Archiver Team
|
||||
AppPublisherURL=https://github.com/yt-dlp/yt-dlp
|
||||
AppSupportURL=https://github.com/yt-dlp/yt-dlp
|
||||
DefaultDirName={autopf}\YouTubeChannelArchiver
|
||||
DefaultGroupName=YouTube Channel Archiver
|
||||
AllowNoIcons=yes
|
||||
LicenseFile=LICENSE.txt
|
||||
OutputDir=installer_output
|
||||
OutputBaseFilename=YouTubeChannelArchiver_v1.0_Setup
|
||||
SetupIconFile=icon.ico
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
PrivilegesRequired=lowest
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "dist\YouTubeChannelArchiver.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "README.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme
|
||||
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\YouTube Channel Archiver"; Filename: "{app}\YouTubeChannelArchiver.exe"
|
||||
Name: "{group}\{cm:ProgramOnTheWeb,YouTube Channel Archiver}"; Filename: "https://github.com/yt-dlp/yt-dlp"
|
||||
Name: "{group}\{cm:UninstallProgram,YouTube Channel Archiver}"; Filename: "{uninstallexe}"
|
||||
Name: "{autodesktop}\YouTube Channel Archiver"; Filename: "{app}\YouTubeChannelArchiver.exe"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\YouTubeChannelArchiver.exe"; Description: "{cm:LaunchProgram,YouTube Channel Archiver}"; Flags: nowait postinstall skipifsilent
|
BIN
windows/python.msix
Normal file
BIN
windows/python.msix
Normal file
Binary file not shown.
867
windows/youtube-channel-archiver-standalone.py
Normal file
867
windows/youtube-channel-archiver-standalone.py
Normal file
@@ -0,0 +1,867 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
YouTube Channel Archiver - Standalone Windows Executable
|
||||
Fixed version that avoids subprocess recursion issues with PyInstaller.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import subprocess
|
||||
import threading
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
import queue
|
||||
import tempfile
|
||||
|
||||
class YouTubeArchiverStandalone:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("YouTube Channel Archiver v1.0")
|
||||
self.root.geometry("700x600")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# Queue for thread communication
|
||||
self.output_queue = queue.Queue()
|
||||
|
||||
# Variables
|
||||
self.channel_url = tk.StringVar()
|
||||
self.output_dir = tk.StringVar(value=str(Path.home() / "Downloads" / "YouTube_Archives"))
|
||||
self.quality = tk.StringVar(value="best")
|
||||
self.audio_only = tk.BooleanVar()
|
||||
self.download_thumbnails = tk.BooleanVar(value=True)
|
||||
self.download_metadata = tk.BooleanVar(value=True)
|
||||
|
||||
# Track processes
|
||||
self.download_process = None
|
||||
self.is_downloading = False
|
||||
self.dependencies_checked = False
|
||||
self.total_videos = 0
|
||||
self.downloaded_videos = 0
|
||||
self.current_video = ""
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
# Check dependencies in background
|
||||
self.root.after(1000, self.check_all_dependencies)
|
||||
|
||||
# Start checking queue for output updates
|
||||
self.root.after(100, self.check_queue)
|
||||
|
||||
def get_python_executable(self):
|
||||
"""Get the actual Python executable path, not the PyInstaller executable."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as PyInstaller executable
|
||||
# Try to find Python in common locations
|
||||
possible_paths = [
|
||||
'python',
|
||||
'python3',
|
||||
'py',
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Python', 'Python*', 'python.exe'),
|
||||
os.path.join(os.environ.get('PROGRAMFILES', ''), 'Python*', 'python.exe'),
|
||||
os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Python*', 'python.exe'),
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
try:
|
||||
result = subprocess.run([path, '--version'],
|
||||
capture_output=True, text=True, check=True, timeout=5)
|
||||
if 'Python' in result.stdout:
|
||||
return path
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
|
||||
# If we can't find Python, we'll have to ask the user
|
||||
return None
|
||||
else:
|
||||
# Running as normal Python script
|
||||
return sys.executable
|
||||
|
||||
def create_widgets(self):
|
||||
# Main frame with padding
|
||||
main_frame = ttk.Frame(self.root, padding="15")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Configure grid weights
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Title and info
|
||||
title_frame = ttk.Frame(main_frame)
|
||||
title_frame.grid(row=0, column=0, columnspan=3, pady=(0, 20))
|
||||
|
||||
title_label = ttk.Label(title_frame, text="YouTube Channel Archiver",
|
||||
font=('Arial', 18, 'bold'))
|
||||
title_label.pack()
|
||||
|
||||
subtitle_label = ttk.Label(title_frame, text="Download entire YouTube channels with ease",
|
||||
font=('Arial', 10))
|
||||
subtitle_label.pack()
|
||||
|
||||
# Status frame
|
||||
status_frame = ttk.LabelFrame(main_frame, text="System Status", padding="10")
|
||||
status_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
|
||||
status_frame.columnconfigure(1, weight=1)
|
||||
|
||||
self.python_status = ttk.Label(status_frame, text="Python: Checking...", foreground="orange")
|
||||
self.python_status.grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
self.ytdlp_status = ttk.Label(status_frame, text="yt-dlp: Checking...", foreground="orange")
|
||||
self.ytdlp_status.grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
# Channel URL input
|
||||
ttk.Label(main_frame, text="YouTube Channel URL:", font=('Arial', 10, 'bold')).grid(
|
||||
row=2, column=0, sticky=tk.W, pady=(5, 2))
|
||||
|
||||
url_frame = ttk.Frame(main_frame)
|
||||
url_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
url_frame.columnconfigure(0, weight=1)
|
||||
|
||||
url_entry = ttk.Entry(url_frame, textvariable=self.channel_url, font=('Arial', 10))
|
||||
url_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
|
||||
|
||||
paste_btn = ttk.Button(url_frame, text="Paste", command=self.paste_from_clipboard)
|
||||
paste_btn.grid(row=0, column=1)
|
||||
|
||||
# Examples
|
||||
examples_label = ttk.Label(main_frame,
|
||||
text="Examples: https://www.youtube.com/@channelname or https://www.youtube.com/c/channelname",
|
||||
font=('Arial', 8), foreground="gray")
|
||||
examples_label.grid(row=4, column=0, columnspan=3, sticky=tk.W)
|
||||
|
||||
# Output directory
|
||||
ttk.Label(main_frame, text="Download Location:", font=('Arial', 10, 'bold')).grid(
|
||||
row=5, column=0, sticky=tk.W, pady=(15, 2))
|
||||
|
||||
dir_frame = ttk.Frame(main_frame)
|
||||
dir_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
dir_frame.columnconfigure(0, weight=1)
|
||||
|
||||
dir_entry = ttk.Entry(dir_frame, textvariable=self.output_dir, font=('Arial', 10))
|
||||
dir_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
|
||||
|
||||
browse_btn = ttk.Button(dir_frame, text="Browse", command=self.browse_directory)
|
||||
browse_btn.grid(row=0, column=1)
|
||||
|
||||
# Options frame
|
||||
options_frame = ttk.LabelFrame(main_frame, text="Download Options", padding="10")
|
||||
options_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
|
||||
options_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Quality selection
|
||||
ttk.Label(options_frame, text="Video Quality:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
quality_combo = ttk.Combobox(options_frame, textvariable=self.quality,
|
||||
values=["best", "1080p", "720p", "480p", "worst"],
|
||||
state="readonly", width=15)
|
||||
quality_combo.grid(row=0, column=1, sticky=tk.W, pady=2, padx=(10, 0))
|
||||
|
||||
# Checkboxes
|
||||
ttk.Checkbutton(options_frame, text="Audio only",
|
||||
variable=self.audio_only).grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Checkbutton(options_frame, text="Download thumbnails",
|
||||
variable=self.download_thumbnails).grid(row=1, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Checkbutton(options_frame, text="Download metadata",
|
||||
variable=self.download_metadata).grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
# Manual install button (for when auto-install fails)
|
||||
self.manual_install_btn = ttk.Button(options_frame, text="Install yt-dlp Manually",
|
||||
command=self.show_manual_install_instructions)
|
||||
self.manual_install_btn.grid(row=2, column=1, sticky=tk.W, pady=2)
|
||||
self.manual_install_btn.grid_remove() # Hide initially
|
||||
|
||||
# Download button
|
||||
self.download_btn = ttk.Button(main_frame, text="Download Channel",
|
||||
command=self.start_download,
|
||||
style="Accent.TButton")
|
||||
self.download_btn.grid(row=8, column=0, columnspan=3, pady=15)
|
||||
self.download_btn.state(['disabled']) # Disabled until dependencies are ready
|
||||
|
||||
# Progress bar
|
||||
self.progress = ttk.Progressbar(main_frame, mode='determinate', maximum=100)
|
||||
self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5))
|
||||
|
||||
# Progress label
|
||||
self.progress_label = ttk.Label(main_frame, text="", font=('Arial', 9))
|
||||
self.progress_label.grid(row=10, column=0, columnspan=3, pady=(0, 10))
|
||||
|
||||
# Output text area
|
||||
output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5")
|
||||
output_frame.grid(row=11, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
|
||||
output_frame.columnconfigure(0, weight=1)
|
||||
output_frame.rowconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(11, weight=1)
|
||||
|
||||
self.output_text = scrolledtext.ScrolledText(output_frame, height=10, width=80,
|
||||
font=('Consolas', 9))
|
||||
self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Status bar
|
||||
self.status_var = tk.StringVar(value="Checking system dependencies...")
|
||||
status_bar = ttk.Label(main_frame, textvariable=self.status_var,
|
||||
relief=tk.SUNKEN, anchor=tk.W)
|
||||
status_bar.grid(row=12, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))
|
||||
|
||||
def paste_from_clipboard(self):
|
||||
"""Paste URL from clipboard."""
|
||||
try:
|
||||
clipboard_content = self.root.clipboard_get()
|
||||
if "youtube.com" in clipboard_content or "youtu.be" in clipboard_content:
|
||||
self.channel_url.set(clipboard_content.strip())
|
||||
else:
|
||||
messagebox.showwarning("Invalid URL", "Clipboard doesn't contain a YouTube URL")
|
||||
except tk.TkError:
|
||||
messagebox.showwarning("Clipboard Error", "Could not access clipboard")
|
||||
|
||||
def log_output(self, message):
|
||||
"""Add message to output text area."""
|
||||
self.output_text.insert(tk.END, message + "\n")
|
||||
self.output_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def clear_output(self):
|
||||
"""Clear the output text area."""
|
||||
self.output_text.delete(1.0, tk.END)
|
||||
|
||||
def check_all_dependencies(self):
|
||||
"""Check all system dependencies."""
|
||||
self.log_output("=== System Dependency Check ===")
|
||||
|
||||
# Check Python
|
||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
self.log_output(f"Python {python_version} detected")
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
self.log_output("Running as compiled executable")
|
||||
python_exe = self.get_python_executable()
|
||||
if python_exe:
|
||||
self.log_output(f"Found Python at: {python_exe}")
|
||||
self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green")
|
||||
else:
|
||||
self.log_output("WARNING: Could not locate Python executable for package installation")
|
||||
self.python_status.config(text="Python: Limited functionality", foreground="orange")
|
||||
else:
|
||||
self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green")
|
||||
|
||||
# Check yt-dlp in background thread
|
||||
threading.Thread(target=self.check_ytdlp, daemon=True).start()
|
||||
|
||||
def check_ytdlp(self):
|
||||
"""Check if yt-dlp is available, install if needed."""
|
||||
try:
|
||||
# Try to run yt-dlp
|
||||
result = subprocess.run(['yt-dlp', '--version'],
|
||||
capture_output=True, text=True, check=True, timeout=10)
|
||||
version = result.stdout.strip()
|
||||
|
||||
# Update UI from main thread
|
||||
self.root.after(0, lambda: self.ytdlp_found(version))
|
||||
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
# yt-dlp not found, try to install
|
||||
self.root.after(0, lambda: self.install_ytdlp())
|
||||
|
||||
def ytdlp_found(self, version):
|
||||
"""Called when yt-dlp is found."""
|
||||
self.log_output(f"yt-dlp {version} is available")
|
||||
self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green")
|
||||
self.dependencies_ready()
|
||||
|
||||
def install_ytdlp(self):
|
||||
"""Install yt-dlp using pip."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as executable - need to find Python
|
||||
python_exe = self.get_python_executable()
|
||||
if not python_exe:
|
||||
self.log_output("Cannot install yt-dlp automatically - Python not found")
|
||||
self.ytdlp_status.config(text="yt-dlp: Manual installation required", foreground="red")
|
||||
self.manual_install_btn.grid() # Show manual install button
|
||||
self.show_manual_install_instructions()
|
||||
return
|
||||
else:
|
||||
python_exe = sys.executable
|
||||
|
||||
self.log_output("yt-dlp not found. Installing...")
|
||||
self.ytdlp_status.config(text="yt-dlp: Installing...", foreground="orange")
|
||||
|
||||
# Install in background thread
|
||||
threading.Thread(target=self.do_install_ytdlp, args=(python_exe,), daemon=True).start()
|
||||
|
||||
def do_install_ytdlp(self, python_exe):
|
||||
"""Actually install yt-dlp."""
|
||||
try:
|
||||
# Install yt-dlp
|
||||
self.root.after(0, lambda: self.log_output(f"Installing yt-dlp using: {python_exe}"))
|
||||
|
||||
result = subprocess.run([python_exe, '-m', 'pip', 'install', 'yt-dlp'],
|
||||
capture_output=True, text=True, check=True, timeout=120)
|
||||
|
||||
self.root.after(0, lambda: self.log_output("Installation completed, verifying..."))
|
||||
|
||||
# Try multiple methods to verify yt-dlp installation
|
||||
version = self.verify_ytdlp_installation(python_exe)
|
||||
|
||||
if version:
|
||||
# Update UI from main thread
|
||||
self.root.after(0, lambda: self.ytdlp_installed(version))
|
||||
else:
|
||||
# Installation succeeded but verification failed - that's still OK
|
||||
self.root.after(0, lambda: self.ytdlp_install_partial())
|
||||
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
error_msg = str(e)
|
||||
if hasattr(e, 'stderr') and e.stderr:
|
||||
error_msg += f"\nError output: {e.stderr[:200]}"
|
||||
self.root.after(0, lambda: self.ytdlp_install_failed(error_msg))
|
||||
|
||||
def verify_ytdlp_installation(self, python_exe):
|
||||
"""Try multiple methods to verify yt-dlp is installed and get its version."""
|
||||
|
||||
# Method 1: Try direct yt-dlp command
|
||||
try:
|
||||
result = subprocess.run(['yt-dlp', '--version'],
|
||||
capture_output=True, text=True, check=True, timeout=10)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Method 2: Try through Python module
|
||||
try:
|
||||
result = subprocess.run([python_exe, '-m', 'yt_dlp', '--version'],
|
||||
capture_output=True, text=True, check=True, timeout=10)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Method 3: Try to import and get version programmatically
|
||||
try:
|
||||
result = subprocess.run([python_exe, '-c', 'import yt_dlp; print(yt_dlp.version.__version__)'],
|
||||
capture_output=True, text=True, check=True, timeout=10)
|
||||
version = result.stdout.strip()
|
||||
if version:
|
||||
return version
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Method 4: Check if module can be imported (basic verification)
|
||||
try:
|
||||
result = subprocess.run([python_exe, '-c', 'import yt_dlp; print("installed")'],
|
||||
capture_output=True, text=True, check=True, timeout=10)
|
||||
if 'installed' in result.stdout:
|
||||
return "installed (version unknown)"
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def ytdlp_install_partial(self):
|
||||
"""Called when yt-dlp was installed but can't be verified normally."""
|
||||
self.log_output("yt-dlp installed successfully!")
|
||||
self.log_output("Note: yt-dlp command may need to be accessed via 'python -m yt_dlp'")
|
||||
self.ytdlp_status.config(text="yt-dlp: Installed [OK]", foreground="green")
|
||||
self.dependencies_ready()
|
||||
|
||||
def ytdlp_installed(self, version):
|
||||
"""Called when yt-dlp is successfully installed."""
|
||||
self.log_output(f"yt-dlp {version} installed successfully!")
|
||||
self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green")
|
||||
self.dependencies_ready()
|
||||
|
||||
def ytdlp_install_failed(self, error):
|
||||
"""Called when yt-dlp installation fails."""
|
||||
self.log_output(f"Failed to install yt-dlp: {error}")
|
||||
self.ytdlp_status.config(text="yt-dlp: Installation failed [ERROR]", foreground="red")
|
||||
self.status_var.set("Error: Could not install yt-dlp automatically")
|
||||
self.manual_install_btn.grid() # Show manual install button
|
||||
|
||||
messagebox.showerror("Installation Error",
|
||||
"Could not install yt-dlp automatically.\n\n"
|
||||
"This can happen when running as an executable.\n"
|
||||
"Please click 'Install yt-dlp Manually' for instructions.")
|
||||
|
||||
def show_manual_install_instructions(self):
|
||||
"""Show manual installation instructions."""
|
||||
instructions = """Manual yt-dlp Installation Instructions:
|
||||
|
||||
1. Open Command Prompt or PowerShell as Administrator
|
||||
2. Run one of these commands:
|
||||
|
||||
pip install yt-dlp
|
||||
|
||||
OR
|
||||
|
||||
python -m pip install yt-dlp
|
||||
|
||||
OR
|
||||
|
||||
py -m pip install yt-dlp
|
||||
|
||||
3. After installation, restart this application
|
||||
|
||||
Alternative method:
|
||||
- Download yt-dlp.exe from: https://github.com/yt-dlp/yt-dlp/releases
|
||||
- Place it in the same folder as this application
|
||||
- Or place it in a folder that's in your system PATH
|
||||
|
||||
The application will automatically detect yt-dlp once it's installed."""
|
||||
|
||||
# Create instruction window
|
||||
instruction_window = tk.Toplevel(self.root)
|
||||
instruction_window.title("Manual Installation Instructions")
|
||||
instruction_window.geometry("600x400")
|
||||
instruction_window.transient(self.root)
|
||||
instruction_window.grab_set()
|
||||
|
||||
frame = ttk.Frame(instruction_window, padding="15")
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(frame, height=20, width=70, font=('Consolas', 10))
|
||||
text_widget.pack(fill="both", expand=True)
|
||||
text_widget.insert(tk.END, instructions)
|
||||
text_widget.config(state=tk.DISABLED)
|
||||
|
||||
button_frame = ttk.Frame(frame)
|
||||
button_frame.pack(fill="x", pady=(10, 0))
|
||||
|
||||
ttk.Button(button_frame, text="Recheck Dependencies",
|
||||
command=lambda: [instruction_window.destroy(), self.recheck_dependencies()]).pack(side="right", padx=(5, 0))
|
||||
ttk.Button(button_frame, text="Close",
|
||||
command=instruction_window.destroy).pack(side="right")
|
||||
|
||||
def recheck_dependencies(self):
|
||||
"""Recheck dependencies after manual installation."""
|
||||
self.dependencies_checked = False
|
||||
self.manual_install_btn.grid_remove()
|
||||
self.download_btn.state(['disabled'])
|
||||
self.status_var.set("Rechecking dependencies...")
|
||||
self.log_output("\n=== Rechecking Dependencies ===")
|
||||
threading.Thread(target=self.check_ytdlp, daemon=True).start()
|
||||
|
||||
def dependencies_ready(self):
|
||||
"""Called when all dependencies are ready."""
|
||||
if not self.dependencies_checked:
|
||||
self.dependencies_checked = True
|
||||
self.download_btn.state(['!disabled'])
|
||||
self.manual_install_btn.grid_remove()
|
||||
self.status_var.set("Ready to download!")
|
||||
self.log_output("=== All dependencies ready! ===")
|
||||
|
||||
def browse_directory(self):
|
||||
"""Open directory browser."""
|
||||
directory = filedialog.askdirectory(initialdir=self.output_dir.get())
|
||||
if directory:
|
||||
self.output_dir.set(directory)
|
||||
|
||||
def validate_inputs(self):
|
||||
"""Validate user inputs."""
|
||||
if not self.channel_url.get().strip():
|
||||
messagebox.showerror("Error", "Please enter a YouTube channel URL")
|
||||
return False
|
||||
|
||||
url = self.channel_url.get().strip()
|
||||
if "youtube.com" not in url and "youtu.be" not in url:
|
||||
messagebox.showerror("Error", "Please enter a valid YouTube URL")
|
||||
return False
|
||||
|
||||
if not self.output_dir.get().strip():
|
||||
messagebox.showerror("Error", "Please select an output directory")
|
||||
return False
|
||||
|
||||
# Check if output directory exists or can be created
|
||||
try:
|
||||
output_path = Path(self.output_dir.get())
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Cannot create output directory: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def build_command(self):
|
||||
"""Build the yt-dlp command based on GUI settings."""
|
||||
# Try to determine the best way to call yt-dlp
|
||||
cmd = self.get_ytdlp_command()
|
||||
|
||||
# Output template
|
||||
output_path = Path(self.output_dir.get())
|
||||
output_template = str(output_path / "%(uploader)s" / "%(upload_date)s - %(title)s.%(ext)s")
|
||||
cmd.extend(['-o', output_template])
|
||||
|
||||
# Quality settings
|
||||
if self.audio_only.get():
|
||||
cmd.extend(['-f', 'bestaudio/best'])
|
||||
else:
|
||||
if self.quality.get() == 'best':
|
||||
cmd.extend(['-f', 'best'])
|
||||
elif self.quality.get() == 'worst':
|
||||
cmd.extend(['-f', 'worst'])
|
||||
else:
|
||||
# Custom quality
|
||||
quality_num = self.quality.get().replace('p', '')
|
||||
cmd.extend(['-f', f'best[height<={quality_num}]'])
|
||||
|
||||
# Additional options
|
||||
cmd.extend([
|
||||
'--ignore-errors',
|
||||
'--no-overwrites',
|
||||
'--continue',
|
||||
])
|
||||
|
||||
# Archive file
|
||||
archive_file = output_path / 'download_archive.txt'
|
||||
cmd.extend(['--download-archive', str(archive_file)])
|
||||
|
||||
# Metadata and thumbnails
|
||||
if self.download_metadata.get():
|
||||
cmd.extend(['--write-info-json', '--write-description'])
|
||||
|
||||
if self.download_thumbnails.get():
|
||||
cmd.extend(['--write-thumbnail'])
|
||||
|
||||
# Add channel URL
|
||||
cmd.append(self.channel_url.get().strip())
|
||||
|
||||
return cmd
|
||||
|
||||
def get_ytdlp_command(self):
|
||||
"""Determine the best way to call yt-dlp based on what's available."""
|
||||
|
||||
# Method 1: Try direct yt-dlp command
|
||||
try:
|
||||
subprocess.run(['yt-dlp', '--version'],
|
||||
capture_output=True, text=True, check=True, timeout=5)
|
||||
return ['yt-dlp']
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Method 2: Try through Python module
|
||||
python_exe = self.get_python_executable()
|
||||
if python_exe:
|
||||
try:
|
||||
subprocess.run([python_exe, '-m', 'yt_dlp', '--version'],
|
||||
capture_output=True, text=True, check=True, timeout=5)
|
||||
return [python_exe, '-m', 'yt_dlp']
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Method 3: Fallback to direct command (might fail but user will see error)
|
||||
return ['yt-dlp']
|
||||
|
||||
def start_download(self):
|
||||
"""Start the download process."""
|
||||
if self.is_downloading:
|
||||
self.stop_download()
|
||||
return
|
||||
|
||||
if not self.validate_inputs():
|
||||
return
|
||||
|
||||
# Clear previous output and reset progress
|
||||
self.clear_output()
|
||||
self.reset_progress()
|
||||
|
||||
# Update UI
|
||||
self.is_downloading = True
|
||||
self.download_btn.config(text="Stop Download")
|
||||
self.progress.config(mode='indeterminate')
|
||||
self.progress.start()
|
||||
self.progress_label.config(text="Initializing download...")
|
||||
self.status_var.set("Downloading...")
|
||||
|
||||
# Build command
|
||||
cmd = self.build_command()
|
||||
self.log_output(f"Starting download...")
|
||||
self.log_output(f"Channel: {self.channel_url.get()}")
|
||||
self.log_output(f"Output: {self.output_dir.get()}")
|
||||
self.log_output("-" * 50)
|
||||
|
||||
# Start download in separate thread
|
||||
self.download_thread = threading.Thread(target=self.run_download, args=(cmd,))
|
||||
self.download_thread.daemon = True
|
||||
self.download_thread.start()
|
||||
|
||||
def reset_progress(self):
|
||||
"""Reset progress tracking variables."""
|
||||
self.total_videos = 0
|
||||
self.downloaded_videos = 0
|
||||
self.current_video = ""
|
||||
self.progress.config(value=0)
|
||||
self.progress_label.config(text="")
|
||||
|
||||
def update_progress(self, downloaded, total, current_title=""):
|
||||
"""Update the progress bar and label."""
|
||||
self.downloaded_videos = downloaded
|
||||
self.total_videos = total
|
||||
self.current_video = current_title
|
||||
|
||||
if total > 0:
|
||||
percentage = (downloaded / total) * 100
|
||||
self.progress.config(mode='determinate', value=percentage)
|
||||
|
||||
# Truncate long titles
|
||||
display_title = current_title[:50] + "..." if len(current_title) > 50 else current_title
|
||||
|
||||
self.progress_label.config(
|
||||
text=f"Progress: {downloaded}/{total} videos ({percentage:.1f}%) - {display_title}"
|
||||
)
|
||||
else:
|
||||
self.progress.config(mode='indeterminate')
|
||||
self.progress_label.config(text="Analyzing channel...")
|
||||
|
||||
def parse_download_output(self, line):
|
||||
"""Parse yt-dlp output to extract progress information."""
|
||||
line = line.strip()
|
||||
|
||||
# Look for playlist info: [download] Downloading video 5 of 23: Title
|
||||
if "[download] Downloading video" in line:
|
||||
try:
|
||||
# Extract: "Downloading video 5 of 23: Title"
|
||||
parts = line.split(":")
|
||||
if len(parts) >= 2:
|
||||
video_part = parts[1].strip() # "Downloading video 5 of 23"
|
||||
title_part = ":".join(parts[2:]).strip() if len(parts) > 2 else ""
|
||||
|
||||
# Extract numbers: "5 of 23"
|
||||
if " of " in video_part:
|
||||
numbers = video_part.split(" of ")
|
||||
if len(numbers) == 2:
|
||||
try:
|
||||
current = int(numbers[0].split()[-1]) # Get last word before "of"
|
||||
total = int(numbers[1].split()[0]) # Get first word after "of"
|
||||
self.root.after(0, lambda: self.update_progress(current, total, title_part))
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Look for single video download progress
|
||||
elif "[download]" in line and "%" in line:
|
||||
# Extract percentage from lines like: [download] 45.2% of 123.45MiB at 1.23MiB/s ETA 00:30
|
||||
try:
|
||||
if "%" in line:
|
||||
percent_part = line.split("%")[0]
|
||||
percent_value = float(percent_part.split()[-1])
|
||||
|
||||
# If we don't have total videos info, show single video progress
|
||||
if self.total_videos == 0:
|
||||
self.root.after(0, lambda: self.update_single_video_progress(percent_value))
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# Look for playlist detection
|
||||
elif "[youtube] Extracting playlist information" in line or "playlist" in line.lower():
|
||||
self.root.after(0, lambda: self.update_progress(0, 0, "Analyzing playlist..."))
|
||||
|
||||
def update_single_video_progress(self, percentage):
|
||||
"""Update progress for single video download."""
|
||||
self.progress.config(mode='determinate', value=percentage)
|
||||
self.progress_label.config(text=f"Download progress: {percentage:.1f}%")
|
||||
|
||||
def show_completion_notification(self, success=True, message=""):
|
||||
"""Show a popup notification when download completes."""
|
||||
if success:
|
||||
title = "Download Complete!"
|
||||
icon = "info"
|
||||
msg = f"Channel download completed successfully!\n\n"
|
||||
if self.total_videos > 0:
|
||||
msg += f"Downloaded {self.downloaded_videos} out of {self.total_videos} videos.\n"
|
||||
msg += f"Files saved to:\n{self.output_dir.get()}"
|
||||
else:
|
||||
title = "Download Finished"
|
||||
icon = "warning"
|
||||
msg = f"Download finished with some issues.\n\n{message}\n\n"
|
||||
msg += f"Files saved to:\n{self.output_dir.get()}"
|
||||
|
||||
# Create custom notification window
|
||||
notification = tk.Toplevel(self.root)
|
||||
notification.title(title)
|
||||
notification.geometry("450x250")
|
||||
notification.resizable(False, False)
|
||||
notification.transient(self.root)
|
||||
notification.grab_set()
|
||||
|
||||
# Center the notification
|
||||
notification.update_idletasks()
|
||||
x = (notification.winfo_screenwidth() // 2) - (notification.winfo_width() // 2)
|
||||
y = (notification.winfo_screenheight() // 2) - (notification.winfo_height() // 2)
|
||||
notification.geometry(f"+{x}+{y}")
|
||||
|
||||
# Configure notification content
|
||||
frame = ttk.Frame(notification, padding="20")
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(frame, text=title, font=('Arial', 14, 'bold'))
|
||||
title_label.pack(pady=(0, 10))
|
||||
|
||||
# Message
|
||||
message_label = ttk.Label(frame, text=msg, wraplength=400, justify="center")
|
||||
message_label.pack(pady=(0, 15))
|
||||
|
||||
# Buttons
|
||||
button_frame = ttk.Frame(frame)
|
||||
button_frame.pack(fill="x", pady=(10, 0))
|
||||
|
||||
def open_folder():
|
||||
"""Open the download folder."""
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(self.output_dir.get())
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
subprocess.run(["open", self.output_dir.get()])
|
||||
else: # Linux
|
||||
subprocess.run(["xdg-open", self.output_dir.get()])
|
||||
notification.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Could not open folder: {e}")
|
||||
|
||||
ttk.Button(button_frame, text="Open Folder", command=open_folder).pack(side="left", padx=(0, 10))
|
||||
ttk.Button(button_frame, text="OK", command=notification.destroy).pack(side="right")
|
||||
|
||||
# Auto-close after 30 seconds
|
||||
notification.after(30000, notification.destroy)
|
||||
|
||||
# Play system notification sound
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
import winsound
|
||||
winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS)
|
||||
except ImportError:
|
||||
pass # No sound on non-Windows or if winsound not available
|
||||
|
||||
def run_download(self, cmd):
|
||||
"""Run the download process."""
|
||||
try:
|
||||
# Start the process
|
||||
self.download_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
|
||||
)
|
||||
|
||||
# Read output line by line
|
||||
for line in iter(self.download_process.stdout.readline, ''):
|
||||
if not self.is_downloading:
|
||||
break
|
||||
|
||||
# Parse the line for progress information
|
||||
self.parse_download_output(line)
|
||||
|
||||
# Send line to output display
|
||||
self.output_queue.put(('output', line.rstrip()))
|
||||
|
||||
# Wait for process to complete
|
||||
return_code = self.download_process.wait()
|
||||
|
||||
if return_code == 0:
|
||||
self.output_queue.put(('success', 'Download completed successfully!'))
|
||||
else:
|
||||
self.output_queue.put(('warning', f'Download finished with some errors (code {return_code})'))
|
||||
|
||||
except Exception as e:
|
||||
self.output_queue.put(('error', f'Error during download: {str(e)}'))
|
||||
finally:
|
||||
self.output_queue.put(('finished', None))
|
||||
|
||||
def stop_download(self):
|
||||
"""Stop the current download."""
|
||||
if self.download_process and self.download_process.poll() is None:
|
||||
self.download_process.terminate()
|
||||
self.log_output("\n[WARNING] Download stopped by user")
|
||||
|
||||
self.download_finished()
|
||||
|
||||
def download_finished(self):
|
||||
"""Handle download completion."""
|
||||
self.is_downloading = False
|
||||
self.download_btn.config(text="Download Channel")
|
||||
self.progress.stop()
|
||||
self.progress.config(mode='determinate')
|
||||
self.status_var.set("Ready to download!")
|
||||
|
||||
def check_queue(self):
|
||||
"""Check the queue for messages from the download thread."""
|
||||
try:
|
||||
while True:
|
||||
msg_type, message = self.output_queue.get_nowait()
|
||||
|
||||
if msg_type == 'output':
|
||||
self.log_output(message)
|
||||
elif msg_type == 'success':
|
||||
self.log_output(f"\n{message}")
|
||||
self.status_var.set(message)
|
||||
self.show_completion_notification(success=True)
|
||||
elif msg_type == 'warning':
|
||||
self.log_output(f"\n{message}")
|
||||
self.status_var.set(message)
|
||||
self.show_completion_notification(success=False, message=message)
|
||||
elif msg_type == 'error':
|
||||
self.log_output(f"\n[ERROR] {message}")
|
||||
messagebox.showerror("Download Error", message)
|
||||
elif msg_type == 'finished':
|
||||
self.download_finished()
|
||||
break
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
# Schedule next check
|
||||
self.root.after(100, self.check_queue)
|
||||
|
||||
def main():
|
||||
# Prevent recursive calls when frozen
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as PyInstaller executable
|
||||
import multiprocessing
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
# Set up the main window
|
||||
root = tk.Tk()
|
||||
|
||||
# Configure styling
|
||||
style = ttk.Style()
|
||||
try:
|
||||
style.theme_use('vista') # Windows native theme
|
||||
except:
|
||||
try:
|
||||
style.theme_use('clam') # Fallback modern theme
|
||||
except:
|
||||
pass # Use default
|
||||
|
||||
# Configure button style
|
||||
style.configure('Accent.TButton', font=('Arial', 10, 'bold'))
|
||||
|
||||
# Create the application
|
||||
app = YouTubeArchiverStandalone(root)
|
||||
|
||||
# Handle window closing
|
||||
def on_closing():
|
||||
if app.is_downloading:
|
||||
if messagebox.askokcancel("Quit", "Download in progress. Stop and quit?"):
|
||||
app.stop_download()
|
||||
root.destroy()
|
||||
else:
|
||||
root.destroy()
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", on_closing)
|
||||
|
||||
# Center window on screen
|
||||
root.update_idletasks()
|
||||
x = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
|
||||
y = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
# Set minimum size
|
||||
root.minsize(600, 500)
|
||||
|
||||
# Run the application
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -17,7 +17,7 @@ def check_dependencies():
|
||||
try:
|
||||
result = subprocess.run(['yt-dlp', '--version'],
|
||||
capture_output=True, text=True, check=True)
|
||||
print(f"✓ yt-dlp is installed: {result.stdout.strip()}")
|
||||
print(f"[OK] yt-dlp is installed: {result.stdout.strip()}")
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
@@ -28,10 +28,10 @@ def install_ytdlp():
|
||||
try:
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', 'yt-dlp'],
|
||||
check=True)
|
||||
print("✓ yt-dlp installed successfully!")
|
||||
print("[OK] yt-dlp installed successfully!")
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("✗ Failed to install yt-dlp automatically.")
|
||||
print("[ERROR] Failed to install yt-dlp automatically.")
|
||||
print("Please install yt-dlp manually:")
|
||||
print(" pip install yt-dlp")
|
||||
print(" or visit: https://github.com/yt-dlp/yt-dlp")
|
||||
@@ -127,15 +127,15 @@ def download_channel(channel_url, output_dir, quality='best', audio_only=False,
|
||||
# Run the download command
|
||||
subprocess.run(cmd, check=True, cwd=output_dir)
|
||||
print("-" * 50)
|
||||
print("✓ Download completed successfully!")
|
||||
print("[OK] Download completed successfully!")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Download failed with error code: {e.returncode}")
|
||||
print(f"[ERROR] Download failed with error code: {e.returncode}")
|
||||
print("Some videos may have been downloaded successfully.")
|
||||
return False
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠ Download interrupted by user.")
|
||||
print("\n[WARNING] Download interrupted by user.")
|
||||
print("You can resume later - already downloaded videos won't be re-downloaded.")
|
||||
return False
|
||||
|
||||
@@ -193,7 +193,7 @@ Examples:
|
||||
output_dir = create_download_directory(args.channel_url, args.output)
|
||||
print(f"Download directory: {output_dir}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error creating download directory: {e}")
|
||||
print(f"[ERROR] Error creating download directory: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Start download
|
||||
@@ -207,7 +207,7 @@ Examples:
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"\n✓ All videos downloaded to: {output_dir}")
|
||||
print(f"\n[OK] All videos downloaded to: {output_dir}")
|
||||
print(f"Archive file created at: {output_dir / 'download_archive.txt'}")
|
||||
else:
|
||||
sys.exit(1)
|
509
youtube_archiver_gui.py
Normal file
509
youtube_archiver_gui.py
Normal file
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
YouTube Channel Archiver GUI
|
||||
A simple graphical interface for the YouTube channel archiver script.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import subprocess
|
||||
import threading
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
import queue
|
||||
|
||||
class YouTubeArchiverGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("YouTube Channel Archiver")
|
||||
self.root.geometry("600x500")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# Queue for thread communication
|
||||
self.output_queue = queue.Queue()
|
||||
|
||||
# Variables
|
||||
self.channel_url = tk.StringVar()
|
||||
self.output_dir = tk.StringVar(value=str(Path.cwd()))
|
||||
self.quality = tk.StringVar(value="best")
|
||||
self.audio_only = tk.BooleanVar()
|
||||
self.download_thumbnails = tk.BooleanVar(value=True)
|
||||
self.download_metadata = tk.BooleanVar(value=True)
|
||||
|
||||
# Track download process
|
||||
self.download_process = None
|
||||
self.is_downloading = False
|
||||
|
||||
# Check Python installation on Windows first
|
||||
if not self.check_python_installation():
|
||||
return # Exit if Python installation failed
|
||||
|
||||
self.create_widgets()
|
||||
self.check_dependencies()
|
||||
|
||||
# Start checking queue for output updates
|
||||
self.root.after(100, self.check_queue)
|
||||
|
||||
def check_python_installation(self):
|
||||
"""Check if Python is properly installed on Windows."""
|
||||
if platform.system() != "Windows":
|
||||
return True # Not Windows, proceed normally
|
||||
|
||||
try:
|
||||
# Try to run python and get version
|
||||
result = subprocess.run([sys.executable, "--version"],
|
||||
capture_output=True, text=True, check=True)
|
||||
python_version = result.stdout.strip()
|
||||
print(f"Python is available: {python_version}")
|
||||
return True
|
||||
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
# Python not found, try to install it
|
||||
return self.install_python_windows()
|
||||
|
||||
def install_python_windows(self):
|
||||
"""Install Python on Windows using the provided MSIX installer."""
|
||||
installer_path = Path("windows/python.msix")
|
||||
|
||||
if not installer_path.exists():
|
||||
messagebox.showerror(
|
||||
"Python Installation Error",
|
||||
f"Python is not installed and the installer was not found at:\n{installer_path.absolute()}\n\n"
|
||||
"Please install Python manually from python.org or place the installer at the expected location."
|
||||
)
|
||||
return False
|
||||
|
||||
# Show installation dialog
|
||||
result = messagebox.askyesno(
|
||||
"Python Installation Required",
|
||||
"Python is not installed on this system. Would you like to install it now?\n\n"
|
||||
f"The installer will be launched from:\n{installer_path.absolute()}"
|
||||
)
|
||||
|
||||
if not result:
|
||||
messagebox.showinfo(
|
||||
"Installation Cancelled",
|
||||
"Python installation cancelled. The application cannot run without Python."
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Create a temporary window to show installation progress
|
||||
install_window = tk.Toplevel(self.root)
|
||||
install_window.title("Installing Python")
|
||||
install_window.geometry("400x200")
|
||||
install_window.resizable(False, False)
|
||||
install_window.transient(self.root)
|
||||
install_window.grab_set()
|
||||
|
||||
# Center the window
|
||||
install_window.update_idletasks()
|
||||
x = (install_window.winfo_screenwidth() // 2) - (install_window.winfo_width() // 2)
|
||||
y = (install_window.winfo_screenheight() // 2) - (install_window.winfo_height() // 2)
|
||||
install_window.geometry(f"+{x}+{y}")
|
||||
|
||||
# Add progress indicator and message
|
||||
frame = ttk.Frame(install_window, padding="20")
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(frame, text="Installing Python...",
|
||||
font=('Arial', 12, 'bold')).pack(pady=(0, 10))
|
||||
|
||||
progress = ttk.Progressbar(frame, mode='indeterminate')
|
||||
progress.pack(fill="x", pady=(0, 10))
|
||||
progress.start()
|
||||
|
||||
status_label = ttk.Label(frame, text="Launching installer...", wraplength=350)
|
||||
status_label.pack(pady=(0, 10))
|
||||
|
||||
# Update the window
|
||||
install_window.update()
|
||||
|
||||
# Launch the MSIX installer
|
||||
status_label.config(text="Running Python installer. Please follow the installation prompts.")
|
||||
install_window.update()
|
||||
|
||||
# Use PowerShell to install the MSIX package
|
||||
powershell_cmd = f'Add-AppxPackage -Path "{installer_path.absolute()}"'
|
||||
|
||||
result = subprocess.run([
|
||||
"powershell", "-Command", powershell_cmd
|
||||
], capture_output=True, text=True)
|
||||
|
||||
progress.stop()
|
||||
|
||||
if result.returncode == 0:
|
||||
status_label.config(text="Installation completed successfully!")
|
||||
install_window.update()
|
||||
|
||||
# Wait a moment and then verify installation
|
||||
self.root.after(2000, lambda: self.verify_python_installation(install_window))
|
||||
return True
|
||||
else:
|
||||
# Installation failed
|
||||
error_msg = result.stderr or result.stdout or "Unknown error occurred"
|
||||
status_label.config(text=f"Installation failed: {error_msg[:100]}...")
|
||||
install_window.update()
|
||||
|
||||
messagebox.showerror(
|
||||
"Installation Failed",
|
||||
f"Python installation failed:\n{error_msg}\n\n"
|
||||
"Please try installing Python manually from python.org"
|
||||
)
|
||||
install_window.destroy()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror(
|
||||
"Installation Error",
|
||||
f"An error occurred while installing Python:\n{str(e)}\n\n"
|
||||
"Please try installing Python manually from python.org"
|
||||
)
|
||||
if 'install_window' in locals():
|
||||
install_window.destroy()
|
||||
return False
|
||||
|
||||
def verify_python_installation(self, install_window):
|
||||
"""Verify that Python was installed successfully."""
|
||||
try:
|
||||
# Try to find Python in common locations
|
||||
possible_python_paths = [
|
||||
"python",
|
||||
"python3",
|
||||
"py",
|
||||
os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe"),
|
||||
os.path.expandvars(r"%LOCALAPPDATA%\Programs\Python\Python*\python.exe"),
|
||||
]
|
||||
|
||||
python_found = False
|
||||
for python_path in possible_python_paths:
|
||||
try:
|
||||
result = subprocess.run([python_path, "--version"],
|
||||
capture_output=True, text=True, check=True)
|
||||
python_version = result.stdout.strip()
|
||||
print(f"Python found at {python_path}: {python_version}")
|
||||
python_found = True
|
||||
break
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
install_window.destroy()
|
||||
|
||||
if python_found:
|
||||
messagebox.showinfo(
|
||||
"Installation Successful",
|
||||
f"Python has been installed successfully!\n{python_version}\n\n"
|
||||
"The application will now continue."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
messagebox.showwarning(
|
||||
"Installation Verification Failed",
|
||||
"Python installation completed, but Python could not be found in the expected locations.\n\n"
|
||||
"You may need to restart the application or your system for the installation to take effect."
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
install_window.destroy()
|
||||
messagebox.showerror(
|
||||
"Verification Error",
|
||||
f"Could not verify Python installation:\n{str(e)}"
|
||||
)
|
||||
return False
|
||||
|
||||
def create_widgets(self):
|
||||
# Main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Configure grid weights
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(main_frame, text="YouTube Channel Archiver",
|
||||
font=('Arial', 16, 'bold'))
|
||||
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
|
||||
|
||||
# Channel URL input
|
||||
ttk.Label(main_frame, text="Channel URL:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
url_entry = ttk.Entry(main_frame, textvariable=self.channel_url, width=50)
|
||||
url_entry.grid(row=1, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
|
||||
|
||||
# Output directory
|
||||
ttk.Label(main_frame, text="Output Directory:").grid(row=2, column=0, sticky=tk.W, pady=5)
|
||||
dir_entry = ttk.Entry(main_frame, textvariable=self.output_dir, width=40)
|
||||
dir_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 5))
|
||||
|
||||
browse_btn = ttk.Button(main_frame, text="Browse", command=self.browse_directory)
|
||||
browse_btn.grid(row=2, column=2, pady=5)
|
||||
|
||||
# Options frame
|
||||
options_frame = ttk.LabelFrame(main_frame, text="Options", padding="10")
|
||||
options_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
|
||||
options_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Quality selection
|
||||
ttk.Label(options_frame, text="Quality:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
quality_combo = ttk.Combobox(options_frame, textvariable=self.quality,
|
||||
values=["best", "worst", "1080p", "720p", "480p"],
|
||||
state="readonly", width=15)
|
||||
quality_combo.grid(row=0, column=1, sticky=tk.W, pady=2, padx=(10, 0))
|
||||
|
||||
# Checkboxes
|
||||
audio_check = ttk.Checkbutton(options_frame, text="Audio only",
|
||||
variable=self.audio_only)
|
||||
audio_check.grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
thumb_check = ttk.Checkbutton(options_frame, text="Download thumbnails",
|
||||
variable=self.download_thumbnails)
|
||||
thumb_check.grid(row=1, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
meta_check = ttk.Checkbutton(options_frame, text="Download metadata",
|
||||
variable=self.download_metadata)
|
||||
meta_check.grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
# Download button
|
||||
self.download_btn = ttk.Button(main_frame, text="Download Channel",
|
||||
command=self.start_download, style="Accent.TButton")
|
||||
self.download_btn.grid(row=4, column=0, columnspan=3, pady=20)
|
||||
|
||||
# Progress bar
|
||||
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
|
||||
self.progress.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
|
||||
# Output text area
|
||||
output_frame = ttk.LabelFrame(main_frame, text="Output", padding="5")
|
||||
output_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
|
||||
output_frame.columnconfigure(0, weight=1)
|
||||
output_frame.rowconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(6, weight=1)
|
||||
|
||||
self.output_text = scrolledtext.ScrolledText(output_frame, height=12, width=70)
|
||||
self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Status bar
|
||||
self.status_var = tk.StringVar(value="Ready")
|
||||
status_bar = ttk.Label(main_frame, textvariable=self.status_var,
|
||||
relief=tk.SUNKEN, anchor=tk.W)
|
||||
status_bar.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))
|
||||
|
||||
def check_dependencies(self):
|
||||
"""Check if yt-dlp is available."""
|
||||
try:
|
||||
result = subprocess.run(['yt-dlp', '--version'],
|
||||
capture_output=True, text=True, check=True)
|
||||
version = result.stdout.strip()
|
||||
self.log_output(f"✓ yt-dlp is available: {version}")
|
||||
self.status_var.set("Ready - yt-dlp found")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
self.log_output("⚠ yt-dlp not found. It will be installed automatically when needed.")
|
||||
self.status_var.set("Ready - yt-dlp will be auto-installed")
|
||||
|
||||
def browse_directory(self):
|
||||
"""Open directory browser."""
|
||||
directory = filedialog.askdirectory(initialdir=self.output_dir.get())
|
||||
if directory:
|
||||
self.output_dir.set(directory)
|
||||
|
||||
def log_output(self, message):
|
||||
"""Add message to output text area."""
|
||||
self.output_text.insert(tk.END, message + "\n")
|
||||
self.output_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def clear_output(self):
|
||||
"""Clear the output text area."""
|
||||
self.output_text.delete(1.0, tk.END)
|
||||
|
||||
def validate_inputs(self):
|
||||
"""Validate user inputs."""
|
||||
if not self.channel_url.get().strip():
|
||||
messagebox.showerror("Error", "Please enter a channel URL")
|
||||
return False
|
||||
|
||||
if not self.output_dir.get().strip():
|
||||
messagebox.showerror("Error", "Please select an output directory")
|
||||
return False
|
||||
|
||||
# Check if output directory exists or can be created
|
||||
try:
|
||||
output_path = Path(self.output_dir.get())
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Cannot create output directory: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def build_command(self):
|
||||
"""Build the yt-dlp command based on GUI settings."""
|
||||
# Find the archiver script
|
||||
script_path = Path(__file__).parent / "youtube_archiver.py"
|
||||
if not script_path.exists():
|
||||
# If not found, assume it's in the same directory
|
||||
script_path = "youtube_archiver.py"
|
||||
|
||||
cmd = [sys.executable, str(script_path)]
|
||||
|
||||
# Add channel URL
|
||||
cmd.append(self.channel_url.get().strip())
|
||||
|
||||
# Add options
|
||||
cmd.extend(["--output", self.output_dir.get()])
|
||||
cmd.extend(["--quality", self.quality.get()])
|
||||
|
||||
if self.audio_only.get():
|
||||
cmd.append("--audio-only")
|
||||
|
||||
if not self.download_thumbnails.get():
|
||||
cmd.append("--no-thumbnails")
|
||||
|
||||
if not self.download_metadata.get():
|
||||
cmd.append("--no-metadata")
|
||||
|
||||
return cmd
|
||||
|
||||
def start_download(self):
|
||||
"""Start the download process in a separate thread."""
|
||||
if self.is_downloading:
|
||||
self.stop_download()
|
||||
return
|
||||
|
||||
if not self.validate_inputs():
|
||||
return
|
||||
|
||||
# Clear previous output
|
||||
self.clear_output()
|
||||
|
||||
# Update UI
|
||||
self.is_downloading = True
|
||||
self.download_btn.config(text="Stop Download")
|
||||
self.progress.start()
|
||||
self.status_var.set("Downloading...")
|
||||
|
||||
# Build command
|
||||
cmd = self.build_command()
|
||||
self.log_output(f"Starting download with command: {' '.join(cmd)}")
|
||||
self.log_output("-" * 60)
|
||||
|
||||
# Start download in separate thread
|
||||
self.download_thread = threading.Thread(target=self.run_download, args=(cmd,))
|
||||
self.download_thread.daemon = True
|
||||
self.download_thread.start()
|
||||
|
||||
def run_download(self, cmd):
|
||||
"""Run the download process."""
|
||||
try:
|
||||
# Start the process
|
||||
self.download_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
)
|
||||
|
||||
# Read output line by line
|
||||
for line in iter(self.download_process.stdout.readline, ''):
|
||||
if not self.is_downloading: # Check if stopped
|
||||
break
|
||||
# Put output in queue for main thread to handle
|
||||
self.output_queue.put(('output', line.rstrip()))
|
||||
|
||||
# Wait for process to complete
|
||||
return_code = self.download_process.wait()
|
||||
|
||||
if return_code == 0:
|
||||
self.output_queue.put(('status', 'Download completed successfully!'))
|
||||
else:
|
||||
self.output_queue.put(('status', f'Download failed with code {return_code}'))
|
||||
|
||||
except Exception as e:
|
||||
self.output_queue.put(('error', f'Error during download: {str(e)}'))
|
||||
finally:
|
||||
self.output_queue.put(('finished', None))
|
||||
|
||||
def stop_download(self):
|
||||
"""Stop the current download."""
|
||||
if self.download_process and self.download_process.poll() is None:
|
||||
self.download_process.terminate()
|
||||
self.log_output("\n⚠ Download stopped by user")
|
||||
|
||||
self.download_finished()
|
||||
|
||||
def download_finished(self):
|
||||
"""Handle download completion."""
|
||||
self.is_downloading = False
|
||||
self.download_btn.config(text="Download Channel")
|
||||
self.progress.stop()
|
||||
self.status_var.set("Ready")
|
||||
|
||||
def check_queue(self):
|
||||
"""Check the queue for messages from the download thread."""
|
||||
try:
|
||||
while True:
|
||||
msg_type, message = self.output_queue.get_nowait()
|
||||
|
||||
if msg_type == 'output':
|
||||
self.log_output(message)
|
||||
elif msg_type == 'status':
|
||||
self.log_output(f"\n{message}")
|
||||
self.status_var.set(message)
|
||||
elif msg_type == 'error':
|
||||
self.log_output(f"\n❌ {message}")
|
||||
messagebox.showerror("Download Error", message)
|
||||
elif msg_type == 'finished':
|
||||
self.download_finished()
|
||||
break
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
# Schedule next check
|
||||
self.root.after(100, self.check_queue)
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
|
||||
# Set up styling
|
||||
style = ttk.Style()
|
||||
|
||||
# Try to use a modern theme
|
||||
try:
|
||||
style.theme_use('clam') # Modern looking theme
|
||||
except:
|
||||
pass # Use default theme if clam is not available
|
||||
|
||||
app = YouTubeArchiverGUI(root)
|
||||
|
||||
# Only proceed if Python installation check passed
|
||||
if not hasattr(app, 'output_queue'):
|
||||
root.destroy()
|
||||
return
|
||||
|
||||
# Handle window closing
|
||||
def on_closing():
|
||||
if app.is_downloading:
|
||||
if messagebox.askokcancel("Quit", "Download in progress. Do you want to stop and quit?"):
|
||||
app.stop_download()
|
||||
root.destroy()
|
||||
else:
|
||||
root.destroy()
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", on_closing)
|
||||
|
||||
# Center window on screen
|
||||
root.update_idletasks()
|
||||
x = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
|
||||
y = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user