Compare commits

...

10 Commits

9 changed files with 1710 additions and 27 deletions

View File

@@ -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
View 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
View 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
View 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
View 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

Binary file not shown.

View 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()

View File

@@ -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
View 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()