import os import sys import subprocess import json import re from pathlib import Path from datetime import datetime from karaoke_downloader.tracking_manager import TrackingManager, SongStatus, FormatType from karaoke_downloader.id3_utils import add_id3_tags, extract_artist_title from karaoke_downloader.songlist_manager import ( load_songlist, load_songlist_tracking, save_songlist_tracking, is_songlist_song_downloaded, mark_songlist_song_downloaded, normalize_title ) from karaoke_downloader.youtube_utils import get_channel_info, get_playlist_info import logging DATA_DIR = Path("data") class KaraokeDownloader: def __init__(self): self.yt_dlp_path = Path("downloader/yt-dlp.exe") self.downloads_dir = Path("downloads") self.logs_dir = Path("logs") self.downloads_dir.mkdir(exist_ok=True) self.logs_dir.mkdir(exist_ok=True) self.tracker = TrackingManager(tracking_file=DATA_DIR / "karaoke_tracking.json", cache_file=DATA_DIR / "channel_cache.json") self.config = self._load_config() self.songlist_tracking_file = DATA_DIR / "songlist_tracking.json" self.songlist_tracking = load_songlist_tracking(str(self.songlist_tracking_file)) def _load_config(self): config_file = DATA_DIR / "config.json" if config_file.exists(): try: with open(config_file, 'r', encoding='utf-8') as f: return json.load(f) except (json.JSONDecodeError, FileNotFoundError) as e: print(f"Warning: Could not load config.json: {e}") return { "download_settings": { "format": "best[height<=720][ext=mp4]/best[height<=720]/best[ext=mp4]/best", "preferred_resolution": "720p", "audio_format": "mp3", "audio_quality": "0", "subtitle_language": "en", "subtitle_format": "srt", "write_metadata": False, "write_thumbnail": False, "write_description": False, "write_annotations": False, "write_comments": False, "write_subtitles": False, "embed_metadata": False, "add_metadata": False, "continue_downloads": True, "no_overwrites": True, "ignore_errors": True, "no_warnings": False }, "folder_structure": { "downloads_dir": "downloads", "logs_dir": "logs", "tracking_file": str(DATA_DIR / "karaoke_tracking.json") }, "logging": { "level": "INFO", "format": "%(asctime)s - %(levelname)s - %(message)s", "include_console": True, "include_file": True }, "yt_dlp_path": "downloader/yt-dlp.exe" } def reset_channel_downloads(self, channel_name, reset_songlist=False, delete_files=False): """ Reset all tracking and optionally files for a channel. If reset_songlist is False, songlist songs are preserved (tracking and files). If reset_songlist is True, songlist songs for this channel are also reset/deleted. """ print(f"\nšŸ”„ Resetting channel: {channel_name} (reset_songlist={reset_songlist}, delete_files={delete_files})") # Find channel_id from channel_name channel_id = None for pid, playlist in self.tracker.data.get('playlists', {}).items(): if playlist['name'] == channel_name or pid == channel_name: channel_id = pid break if not channel_id: print(f"āŒ Channel '{channel_name}' not found in tracking.") return # Get all songs for this channel songs_to_reset = [] for song_id, song in self.tracker.data.get('songs', {}).items(): if song['playlist_id'] == channel_id: # Check if this is a songlist song artist, title = song.get('artist', ''), song.get('title', song.get('name', '')) key = f"{artist.lower()}_{normalize_title(title)}" is_songlist = key in self.songlist_tracking if is_songlist and not reset_songlist: continue # skip songlist songs if not resetting them songs_to_reset.append((song_id, song, is_songlist)) # Reset tracking and optionally delete files files_preserved = 0 files_deleted = 0 for song_id, song, is_songlist in songs_to_reset: # Remove from main tracking self.tracker.data['songs'][song_id]['status'] = 'NOT_DOWNLOADED' self.tracker.data['songs'][song_id]['formats'] = {} self.tracker.data['songs'][song_id]['last_error'] = '' self.tracker.data['songs'][song_id]['download_attempts'] = 0 self.tracker.data['songs'][song_id]['last_updated'] = None # Remove from songlist tracking if needed if is_songlist and reset_songlist: artist, title = song.get('artist', ''), song.get('title', song.get('name', '')) key = f"{artist.lower()}_{normalize_title(title)}" if key in self.songlist_tracking: del self.songlist_tracking[key] # Delete file if requested if delete_files: file_path = song.get('file_path') if file_path: try: p = Path(file_path) if p.exists(): p.unlink() files_deleted += 1 else: files_preserved += 1 except Exception as e: print(f"āš ļø Could not delete file {file_path}: {e}") # --- FIX: Remove all songlist_tracking entries for this channel if reset_songlist is True --- if reset_songlist: keys_to_remove = [k for k, v in self.songlist_tracking.items() if v.get('channel') == channel_name] for k in keys_to_remove: del self.songlist_tracking[k] # Save changes self.tracker.force_save() save_songlist_tracking(self.songlist_tracking, str(self.songlist_tracking_file)) print(f"āœ… Reset {len(songs_to_reset)} songs for channel '{channel_name}'.") if delete_files: print(f" Files deleted: {files_deleted}, files preserved: {files_preserved}") if not reset_songlist: print(f" Songlist songs were preserved.") # ... (rest of the KaraokeDownloader methods, updated to use DATA_DIR for all data file paths) ... # For brevity, the rest of the class methods should be copied here from the original download_karaoke.py, # updating all references to use the new karaoke_downloader.* imports as needed.