Add channel URL lookup and auto-detection

- Accept YouTube URLs in any format (@handle, /c/, /user/, /channel/)
- Auto-extract channel ID from URLs using yt-dlp
- Auto-detect channel name from URL (optional field)
- Support direct channel ID input (backwards compatible)
- Prevent duplicate channels
- Updated UI with better instructions and examples
- Improved user experience - just paste channel URL
This commit is contained in:
2026-05-08 19:19:41 -04:00
parent b3f378a8ef
commit 3726cb5775
2 changed files with 100 additions and 18 deletions

75
app.py
View File

@@ -7,8 +7,10 @@ Provides a web UI for managing YouTube channel downloads
import os
import subprocess
import json
import re
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse, parse_qs
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
app = Flask(__name__)
@@ -45,6 +47,51 @@ def save_settings(settings):
with open(SETTINGS_FILE, 'w') as f:
json.dump(settings, f, indent=2)
def extract_channel_id(url_or_id):
"""
Extract channel ID from various YouTube URL formats or validate direct ID
Returns tuple: (channel_id, channel_name_hint or None)
"""
# If it looks like a channel ID already (24 characters starting with UC)
if re.match(r'^UC[\w-]{22}$', url_or_id.strip()):
return url_or_id.strip(), None
# Parse as URL
try:
parsed = urlparse(url_or_id)
# Format: youtube.com/channel/CHANNEL_ID
if '/channel/' in parsed.path:
channel_id = parsed.path.split('/channel/')[-1].split('/')[0]
if channel_id:
return channel_id, None
# Format: youtube.com/@handle or youtube.com/c/name or youtube.com/user/name
# These require fetching the page to get the actual channel ID
if parsed.netloc in ['youtube.com', 'www.youtube.com', 'm.youtube.com']:
# Use yt-dlp to extract channel ID from URL
try:
result = subprocess.run(
['yt-dlp', '--dump-json', '--playlist-items', '0', url_or_id],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0 and result.stdout:
data = json.loads(result.stdout.split('\n')[0])
channel_id = data.get('channel_id')
channel_name = data.get('channel')
if channel_id:
return channel_id, channel_name
except:
pass
except:
pass
return None, None
def get_channels():
"""Read channels from channels.txt"""
channels = []
@@ -166,18 +213,38 @@ def channels():
@app.route('/channels/add', methods=['POST'])
def add_channel():
"""Add a new channel"""
channel_id = request.form.get('channel_id', '').strip()
channel_input = request.form.get('channel_id', '').strip()
channel_name = request.form.get('channel_name', '').strip()
if not channel_id or not channel_name:
flash('Channel ID and Name are required', 'error')
if not channel_input:
flash('Channel ID or URL is required', 'error')
return redirect(url_for('channels'))
# Extract channel ID from URL or validate direct ID
channel_id, auto_name = extract_channel_id(channel_input)
if not channel_id:
flash('Invalid channel ID or URL. Please check and try again.', 'error')
return redirect(url_for('channels'))
# Use auto-detected name if no name was provided
if not channel_name and auto_name:
channel_name = auto_name
elif not channel_name:
flash('Channel name is required (could not auto-detect from URL)', 'error')
return redirect(url_for('channels'))
# Check for duplicates
channels = get_channels()
for existing in channels:
if existing['id'] == channel_id:
flash(f'Channel already exists: {existing["name"]}', 'error')
return redirect(url_for('channels'))
channels.append({'id': channel_id, 'name': channel_name})
save_channels(channels)
flash(f'Added channel: {channel_name}', 'success')
flash(f'Added channel: {channel_name} ({channel_id})', 'success')
return redirect(url_for('channels'))
@app.route('/channels/delete/<int:index>')