0
1
mirror of https://github.com/radio95-rnt/RadioPlayer.git synced 2026-02-26 21:53:54 +01:00
This commit is contained in:
2025-08-31 18:03:04 +02:00
parent 955a756e6e
commit a88e1af005

View File

@@ -8,7 +8,7 @@ import shutil
import libcache import libcache
import argparse import argparse
from datetime import datetime from datetime import datetime
from typing import List, Dict, Set, Tuple, Optional from typing import List, Dict, Set, Tuple, Optional, Union
from dataclasses import dataclass from dataclasses import dataclass
# Configuration # Configuration
@@ -35,10 +35,31 @@ class Config:
def is_custom_mode(self) -> bool: def is_custom_mode(self) -> bool:
return self.custom_playlist_file is not None return self.custom_playlist_file is not None
@dataclass
class FileItem:
"""Represents either a single file or a folder containing files."""
name: str
is_folder: bool
files: List[str] # For folders: list of contained audio files, For files: [filename]
@property
def display_name(self) -> str:
"""Name to show in the GUI."""
if self.is_folder:
return f"📁 {self.name}/"
return self.name
@property
def all_files(self) -> List[str]:
"""Get all file paths for playlist operations."""
if self.is_folder:
return [os.path.join(self.name, f) for f in self.files]
return self.files
class FileManager: class FileManager:
@staticmethod @staticmethod
def get_audio_files(directory: str) -> List[str]: def get_audio_files(directory: str) -> List[str]:
"""Get all audio files from the specified directory.""" """Get all audio files from the specified directory (legacy method for compatibility)."""
audio_files = [] audio_files = []
try: try:
for file in os.listdir(directory): for file in os.listdir(directory):
@@ -51,11 +72,73 @@ class FileManager:
except PermissionError: except PermissionError:
print(f"Error: Permission denied for directory '{directory}'.") print(f"Error: Permission denied for directory '{directory}'.")
return [] return []
@staticmethod
def get_file_items(directory: str) -> List[FileItem]:
"""Get all audio files and folders containing audio files as FileItem objects."""
items = []
try:
entries = sorted(os.listdir(directory))
for entry in entries:
full_path = os.path.join(directory, entry)
if os.path.isfile(full_path) and entry.lower().endswith(FORMATS):
# Single audio file
items.append(FileItem(name=entry, is_folder=False, files=[entry]))
elif os.path.isdir(full_path):
# Directory - check for audio files inside
audio_files = []
try:
for file in os.listdir(full_path):
if file.lower().endswith(FORMATS):
audio_files.append(file)
except (PermissionError, FileNotFoundError):
continue
if audio_files:
# Folder contains audio files
items.append(FileItem(name=entry, is_folder=True, files=sorted(audio_files)))
return items
except FileNotFoundError:
print(f"Error: Directory '{directory}' not found.")
return []
except PermissionError:
print(f"Error: Permission denied for directory '{directory}'.")
return []
class SearchManager: class SearchManager:
@staticmethod
def filter_file_items(items: List[FileItem], search_term: str) -> List[FileItem]:
"""Filter and sort FileItem objects based on search term."""
if not search_term:
return items
search_lower = search_term.lower()
# Group items by match type
starts_with = []
contains = []
has_chars = []
for item in items:
item_name_lower = item.name.lower()
if item_name_lower.startswith(search_lower):
starts_with.append(item)
elif search_lower in item_name_lower:
contains.append(item)
elif SearchManager._has_matching_chars(item_name_lower, search_lower):
has_chars.append(item)
# Return sorted results: starts_with first, then contains, then has_chars
return starts_with + contains + has_chars
@staticmethod @staticmethod
def filter_files(files: List[str], search_term: str) -> List[str]: def filter_files(files: List[str], search_term: str) -> List[str]:
"""Filter and sort files based on search term.""" """Filter and sort files based on search term (legacy method for compatibility)."""
if not search_term: if not search_term:
return files return files
@@ -110,10 +193,11 @@ class PlaylistManager:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line: if line:
filename = os.path.basename(line) # Store relative path for comparison
self.custom_playlist_files.add(filename) rel_path = os.path.relpath(line, FILES_DIR) if line.startswith('/') else line
self.custom_playlist_files.add(rel_path)
# In custom mode, we'll use 'day' as the default period for display # In custom mode, we'll use 'day' as the default period for display
playlists["custom"]["day"].add(filename) playlists["custom"]["day"].add(rel_path)
return playlists return playlists
else: else:
# Original functionality for weekly playlists # Original functionality for weekly playlists
@@ -130,21 +214,20 @@ class PlaylistManager:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line: if line:
filename = os.path.basename(line) # Store relative path for comparison
playlists[day][period].add(filename) rel_path = os.path.relpath(line, FILES_DIR) if line.startswith('/') else line
playlists[day][period].add(rel_path)
return playlists return playlists
def update_playlist_file(self, day: str, period: str, filepath: str, add: bool): def update_playlist_file(self, day: str, period: str, file_item: FileItem, add: bool):
"""Update a playlist file by adding or removing a file.""" """Update a playlist file by adding or removing files from a FileItem."""
if self.config.is_custom_mode: if self.config.is_custom_mode:
self._update_custom_playlist(filepath, add) self._update_custom_playlist(file_item, add)
else: else:
self._update_weekly_playlist(day, period, filepath, add) self._update_weekly_playlist(day, period, file_item, add)
def _update_custom_playlist(self, filepath: str, add: bool): def _update_custom_playlist(self, file_item: FileItem, add: bool):
"""Update the custom playlist file.""" """Update the custom playlist file."""
full_filepath = os.path.join(FILES_DIR, filepath)
# Ensure the directory exists # Ensure the directory exists
os.makedirs(os.path.dirname(self.config.custom_playlist_file), exist_ok=True) os.makedirs(os.path.dirname(self.config.custom_playlist_file), exist_ok=True)
@@ -154,22 +237,31 @@ class PlaylistManager:
with open(self.config.custom_playlist_file, 'r') as f: with open(self.config.custom_playlist_file, 'r') as f:
lines = f.read().splitlines() lines = f.read().splitlines()
if add and full_filepath not in lines: # Get full paths for all files in the item
lines.append(full_filepath) full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in file_item.all_files]
self.custom_playlist_files.add(filepath)
elif not add and full_filepath in lines: if add:
lines.remove(full_filepath) for full_filepath in full_filepaths:
self.custom_playlist_files.discard(filepath) if full_filepath not in lines:
lines.append(full_filepath)
# Update tracking set with relative paths
self.custom_playlist_files.update(file_item.all_files)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
# Remove from tracking set
for filepath in file_item.all_files:
self.custom_playlist_files.discard(filepath)
# Write back to file # Write back to file
with open(self.config.custom_playlist_file, 'w') as f: with open(self.config.custom_playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else '')) f.write('\n'.join(lines) + ('\n' if lines else ''))
def _update_weekly_playlist(self, day: str, period: str, filepath: str, add: bool): def _update_weekly_playlist(self, day: str, period: str, file_item: FileItem, add: bool):
"""Update a weekly playlist file (original functionality).""" """Update a weekly playlist file (original functionality)."""
playlist_dir = self.ensure_playlist_dir(day) playlist_dir = self.ensure_playlist_dir(day)
playlist_file = os.path.join(playlist_dir, period) playlist_file = os.path.join(playlist_dir, period)
full_filepath = os.path.join(FILES_DIR, filepath)
if not os.path.exists(playlist_file): if not os.path.exists(playlist_file):
with open(playlist_file, 'w') as f: with open(playlist_file, 'w') as f:
@@ -178,14 +270,29 @@ class PlaylistManager:
with open(playlist_file, 'r') as f: with open(playlist_file, 'r') as f:
lines = f.read().splitlines() lines = f.read().splitlines()
if add and full_filepath not in lines: # Get full paths for all files in the item
lines.append(full_filepath) full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in file_item.all_files]
elif not add and full_filepath in lines:
lines.remove(full_filepath) if add:
for full_filepath in full_filepaths:
if full_filepath not in lines:
lines.append(full_filepath)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
with open(playlist_file, 'w') as f: with open(playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else '')) f.write('\n'.join(lines) + ('\n' if lines else ''))
def is_file_item_in_playlist(self, file_item: FileItem, day: str, period: str, playlists: Dict) -> bool:
"""Check if ALL files from a FileItem are in the specified playlist."""
if not file_item.all_files:
return False
playlist_set = playlists.get(day, {}).get(period, set())
return all(filepath in playlist_set for filepath in file_item.all_files)
def copy_day_to_all(self, playlists: Dict, source_day: str, days: List[str]) -> Dict: def copy_day_to_all(self, playlists: Dict, source_day: str, days: List[str]) -> Dict:
"""Copy all playlists from source day to all other days.""" """Copy all playlists from source day to all other days."""
if self.config.is_custom_mode: if self.config.is_custom_mode:
@@ -210,17 +317,17 @@ class PlaylistManager:
return playlists return playlists
def copy_current_file_to_all(self, playlists: Dict, source_day: str, def copy_current_item_to_all(self, playlists: Dict, source_day: str,
days: List[str], current_file: str) -> Tuple[Dict, bool]: days: List[str], current_item: FileItem) -> Tuple[Dict, bool]:
"""Copy current file's playlist assignments to all other days.""" """Copy current item's playlist assignments to all other days."""
if self.config.is_custom_mode: if self.config.is_custom_mode:
# No-op in custom mode # No-op in custom mode
return playlists, False return playlists, False
source_periods = { # Check which periods the item's files are in
period: current_file in playlists[source_day][period] source_periods = {}
for period in self.periods for period in self.periods:
} source_periods[period] = self.is_file_item_in_playlist(current_item, source_day, period, playlists)
if not any(source_periods.values()): if not any(source_periods.values()):
return playlists, False return playlists, False
@@ -231,13 +338,16 @@ class PlaylistManager:
for period, is_present in source_periods.items(): for period, is_present in source_periods.items():
target_set = playlists[target_day][period] target_set = playlists[target_day][period]
full_path = os.path.join(FILES_DIR, current_file)
if is_present: if is_present:
target_set.add(current_file) # Add all files from the item
target_set.update(current_item.all_files)
else: else:
target_set.discard(current_file) # Remove all files from the item
for filepath in current_item.all_files:
target_set.discard(filepath)
# Update the playlist file
playlist_dir = self.ensure_playlist_dir(target_day) playlist_dir = self.ensure_playlist_dir(target_day)
playlist_file = os.path.join(playlist_dir, period) playlist_file = os.path.join(playlist_dir, period)
@@ -247,11 +357,16 @@ class PlaylistManager:
else: else:
lines = [] lines = []
if is_present and full_path not in lines: full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in current_item.all_files]
lines.append(full_path)
elif not is_present: if is_present:
while full_path in lines: for full_filepath in full_filepaths:
lines.remove(full_path) if full_filepath not in lines:
lines.append(full_filepath)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
with open(playlist_file, 'w') as f: with open(playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else '')) f.write('\n'.join(lines) + ('\n' if lines else ''))
@@ -297,12 +412,12 @@ class TerminalUtils:
class StatsCalculator: class StatsCalculator:
@staticmethod @staticmethod
def calculate_category_percentages(playlists: Dict, current_day: str, config: Config) -> Optional[Tuple]: def calculate_category_percentages(playlists: Dict, current_day: str, config: Config, all_file_items: List[FileItem]) -> Optional[Tuple]:
"""Calculate category distribution percentages.""" """Calculate category distribution percentages."""
if config.is_custom_mode: if config.is_custom_mode:
# In custom mode, show simple stats # In custom mode, show simple stats
custom_files = playlists.get("custom", {}).get("day", set()) custom_files = playlists.get("custom", {}).get("day", set())
total_files = len(FileManager.get_audio_files(FILES_DIR)) total_files = sum(len(item.all_files) for item in all_file_items)
assigned_count = len(custom_files) assigned_count = len(custom_files)
if total_files == 0: if total_files == 0:
@@ -359,10 +474,10 @@ class DisplayManager:
self.config = config self.config = config
def draw_header(self, playlists: Dict, current_day: str, current_day_idx: int, def draw_header(self, playlists: Dict, current_day: str, current_day_idx: int,
days: List[str], term_width: int, force_redraw: bool = False, days: List[str], term_width: int, all_file_items: List[FileItem],
state: InterfaceState = None): force_redraw: bool = False, state: InterfaceState = None):
"""Draw the header, only if content has changed.""" """Draw the header, only if content has changed."""
result = self.stats.calculate_category_percentages(playlists, current_day, self.config) result = self.stats.calculate_category_percentages(playlists, current_day, self.config, all_file_items)
percentages, polskie_percentages, total_pl = result or ({}, {}, 0) percentages, polskie_percentages, total_pl = result or ({}, {}, 0)
if self.config.is_custom_mode: if self.config.is_custom_mode:
@@ -377,7 +492,6 @@ class DisplayManager:
f"{cat[:4].capitalize()}: {percentages.get(cat, 0):.1f}% (P:{polskie_percentages.get(cat, 0):.1f}%)" f"{cat[:4].capitalize()}: {percentages.get(cat, 0):.1f}% (P:{polskie_percentages.get(cat, 0):.1f}%)"
for cat in ['late_night', 'morning', 'day', 'night'] for cat in ['late_night', 'morning', 'day', 'night']
]) ])
# ... (rest of your header logic for weekly mode is fine)
day_bar = " ".join([f"\033[1;44m[{day}]\033[0m" if i == current_day_idx else f"[{day}]" for i, day in enumerate(days)]) day_bar = " ".join([f"\033[1;44m[{day}]\033[0m" if i == current_day_idx else f"[{day}]" for i, day in enumerate(days)])
header_content = (category_bar, day_bar) header_content = (category_bar, day_bar)
@@ -385,12 +499,17 @@ class DisplayManager:
if force_redraw or state.last_header != header_content: if force_redraw or state.last_header != header_content:
self.terminal.move_cursor(1) self.terminal.move_cursor(1)
self.terminal.clear_line() self.terminal.clear_line()
# ... (your printing logic for the header) print("\033[1;37mCategory Distribution:\033[0m".center(term_width), end="", flush=True)
print(header_content[0].center(term_width))
self.terminal.move_cursor(2)
self.terminal.clear_line()
print(header_content[0].center(term_width), end="", flush=True)
if not self.config.is_custom_mode: if not self.config.is_custom_mode:
self.terminal.move_cursor(3) self.terminal.move_cursor(3)
self.terminal.clear_line() self.terminal.clear_line()
print(header_content[1]) print(header_content[1], end="", flush=True)
state.last_header = header_content state.last_header = header_content
def get_header_height(self) -> int: def get_header_height(self) -> int:
@@ -409,7 +528,7 @@ class DisplayManager:
print(f"\033[1;33m{search_display}\033[0m") print(f"\033[1;33m{search_display}\033[0m")
state.last_search = search_term state.last_search = search_term
def draw_files_section(self, audio_files: List[str], playlists: Dict, selected_idx: int, def draw_files_section(self, file_items: List[FileItem], playlists: Dict, selected_idx: int,
current_day: str, scroll_offset: int, term_width: int, term_height: int, current_day: str, scroll_offset: int, term_width: int, term_height: int,
force_redraw: bool = False, state: InterfaceState = None): force_redraw: bool = False, state: InterfaceState = None):
"""Draw the files list, optimized to only redraw when necessary.""" """Draw the files list, optimized to only redraw when necessary."""
@@ -418,13 +537,13 @@ class DisplayManager:
available_lines = term_height - content_start_row available_lines = term_height - content_start_row
start_idx = scroll_offset start_idx = scroll_offset
end_idx = min(start_idx + available_lines, len(audio_files)) end_idx = min(start_idx + available_lines, len(file_items))
# Create a snapshot of the current state to compare against the last one # Create a snapshot of the current state to compare against the last one
files_display_state = ( files_display_state = (
start_idx, end_idx, selected_idx, current_day, start_idx, end_idx, selected_idx, current_day,
# We also need to know if the playlist data for the visible files has changed # We also need to know if the playlist data for the visible items has changed
tuple(f in playlists.get(current_day, {}).get('day', set()) for f in audio_files[start_idx:end_idx]) tuple(self._get_item_playlist_status(item, playlists, current_day) for item in file_items[start_idx:end_idx])
) )
if force_redraw or state.last_files_display != files_display_state: if force_redraw or state.last_files_display != files_display_state:
@@ -440,44 +559,47 @@ class DisplayManager:
print(" ", end="") print(" ", end="")
if self.config.is_custom_mode: if self.config.is_custom_mode:
position_info = f" Custom | File {selected_idx + 1}/{len(audio_files)} " position_info = f" Custom | Item {selected_idx + 1}/{len(file_items)} "
else: else:
position_info = f" {current_day.capitalize()} | File {selected_idx + 1}/{len(audio_files)} " position_info = f" {current_day.capitalize()} | Item {selected_idx + 1}/{len(file_items)} "
padding = term_width - len(position_info) - 2 padding = term_width - len(position_info) - 2
print(position_info.center(padding), end="") print(position_info.center(padding), end="")
if end_idx < len(audio_files): if end_idx < len(file_items):
print("", end="", flush=True) print("", end="", flush=True)
else: else:
print(" ", end="", flush=True) print(" ", end="", flush=True)
# File list # File list
for display_row, idx in enumerate(range(start_idx, end_idx)): for display_row, idx in enumerate(range(start_idx, end_idx)):
file = audio_files[idx] item = file_items[idx]
line_row = content_start_row + display_row line_row = content_start_row + display_row
self.terminal.move_cursor(line_row) self.terminal.move_cursor(line_row)
self.terminal.clear_line() self.terminal.clear_line()
if self.config.is_custom_mode: if self.config.is_custom_mode:
# In custom mode, only show 'C' for custom playlist # In custom mode, only show 'C' for custom playlist
in_custom = file in playlists.get("custom", {}).get("day", set()) in_custom = all(filepath in playlists.get("custom", {}).get("day", set())
for filepath in item.all_files)
c_color = "\033[1;32m" if in_custom else "\033[1;30m" c_color = "\033[1;32m" if in_custom else "\033[1;30m"
row_highlight = "\033[1;44m" if idx == selected_idx else "" row_highlight = "\033[1;44m" if idx == selected_idx else ""
max_filename_length = term_width - 6 max_filename_length = term_width - 6
display_file = file display_name = item.display_name
if len(file) > max_filename_length: if len(display_name) > max_filename_length:
display_file = file[:max_filename_length-3] + "..." display_name = display_name[:max_filename_length-3] + "..."
self.terminal.move_cursor(line_row) print(f"{row_highlight}[{c_color}C\033[0m{row_highlight}] {display_name}\033[0m", end="", flush=True)
self.terminal.clear_line()
print(f"{row_highlight}[{c_color}C\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
else: else:
# Original weekly mode display # Original weekly mode display
in_late_night = file in playlists[current_day]['late_night'] in_late_night = all(filepath in playlists[current_day]['late_night']
in_morning = file in playlists[current_day]['morning'] for filepath in item.all_files)
in_day = file in playlists[current_day]['day'] in_morning = all(filepath in playlists[current_day]['morning']
in_night = file in playlists[current_day]['night'] for filepath in item.all_files)
in_day = all(filepath in playlists[current_day]['day']
for filepath in item.all_files)
in_night = all(filepath in playlists[current_day]['night']
for filepath in item.all_files)
l_color = "\033[1;32m" if in_late_night else "\033[1;30m" l_color = "\033[1;32m" if in_late_night else "\033[1;30m"
m_color = "\033[1;32m" if in_morning else "\033[1;30m" m_color = "\033[1;32m" if in_morning else "\033[1;30m"
@@ -487,13 +609,11 @@ class DisplayManager:
row_highlight = "\033[1;44m" if idx == selected_idx else "" row_highlight = "\033[1;44m" if idx == selected_idx else ""
max_filename_length = term_width - 15 max_filename_length = term_width - 15
display_file = file display_name = item.display_name
if len(file) > max_filename_length: if len(display_name) > max_filename_length:
display_file = file[:max_filename_length-3] + "..." display_name = display_name[:max_filename_length-3] + "..."
self.terminal.move_cursor(line_row) print(f"{row_highlight}[{l_color}L\033[0m{row_highlight}] [{m_color}M\033[0m{row_highlight}] [{d_color}D\033[0m{row_highlight}] [{n_color}N\033[0m{row_highlight}] {display_name}\033[0m", end="", flush=True)
self.terminal.clear_line()
print(f"{row_highlight}[{l_color}L\033[0m{row_highlight}] [{m_color}M\033[0m{row_highlight}] [{d_color}D\033[0m{row_highlight}] [{n_color}N\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
# Clear remaining lines # Clear remaining lines
last_end_idx = state.last_files_display[1] if state.last_files_display else 0 last_end_idx = state.last_files_display[1] if state.last_files_display else 0
@@ -503,113 +623,19 @@ class DisplayManager:
self.terminal.clear_line() self.terminal.clear_line()
state.last_files_display = files_display_state state.last_files_display = files_display_state
# Day bar
day_bar = ""
for i, day in enumerate(days):
if i == current_day_idx:
day_bar += f"\033[1;44m[{day}]\033[0m "
else:
day_bar += f"[{day}] "
header_content = (category_bar, day_bar.strip())
if force_redraw or (state and state.last_header != header_content):
self.terminal.move_cursor(1)
self.terminal.clear_line()
print("\033[1;37mCategory Distribution:\033[0m".center(term_width), end="", flush=True)
self.terminal.move_cursor(2)
self.terminal.clear_line()
print(category_bar.center(term_width), end="", flush=True)
self.terminal.move_cursor(3)
self.terminal.clear_line()
print(day_bar.strip(), end="", flush=True)
if state:
state.last_header = header_content
def draw_search_bar(self, search_term: str, term_width: int, force_redraw: bool = False, def _get_item_playlist_status(self, item: FileItem, playlists: Dict, current_day: str) -> Tuple:
state: InterfaceState = None): """Get playlist status for an item to use in display state comparison."""
"""Draw the search bar.""" if self.config.is_custom_mode:
if force_redraw or (state and state.last_search != search_term): return (all(filepath in playlists.get("custom", {}).get("day", set())
self.terminal.move_cursor(self.get_header_height() + 3) for filepath in item.all_files),)
self.terminal.clear_line() else:
search_display = f"Search: {search_term}" return (
if len(search_display) > term_width - 2: all(filepath in playlists[current_day]['late_night'] for filepath in item.all_files),
search_display = search_display[:term_width - 5] + "..." all(filepath in playlists[current_day]['morning'] for filepath in item.all_files),
print(f"\033[1;33m{search_display}\033[0m", end="", flush=True) all(filepath in playlists[current_day]['day'] for filepath in item.all_files),
all(filepath in playlists[current_day]['night'] for filepath in item.all_files)
if state: )
state.last_search = search_term
def draw_files_section(self, audio_files: List[str], playlists: Dict, selected_idx: int,
current_day: str, scroll_offset: int, term_width: int, term_height: int,
force_redraw: bool = False, state: InterfaceState = None):
"""Draw the files list section."""
available_lines = term_height - 4 - self.get_header_height() # Adjusted for search bar
start_idx = max(0, min(scroll_offset, len(audio_files) - available_lines))
end_idx = min(start_idx + available_lines, len(audio_files))
files_display_state = (start_idx, end_idx, selected_idx, current_day)
if force_redraw or (state and (state.last_files_display != files_display_state or
state.last_selected_idx != selected_idx)):
# Position info line
self.terminal.move_cursor(7)
self.terminal.clear_line()
if start_idx > 0:
print("", end="")
else:
print(" ", end="")
position_info = f" {current_day.capitalize()} | File {selected_idx + 1}/{len(audio_files)} "
padding = term_width - len(position_info) - 2
print(position_info.center(padding), end="")
if end_idx < len(audio_files):
print("", end="", flush=True)
else:
print(" ", end="", flush=True)
# File list
for display_row, idx in enumerate(range(start_idx, end_idx)):
file = audio_files[idx]
line_row = 4 + display_row + self.get_header_height() # Start after header and search
in_late_night = file in playlists[current_day]['late_night']
in_morning = file in playlists[current_day]['morning']
in_day = file in playlists[current_day]['day']
in_night = file in playlists[current_day]['night']
l_color = "\033[1;32m" if in_late_night else "\033[1;30m"
m_color = "\033[1;32m" if in_morning else "\033[1;30m"
d_color = "\033[1;32m" if in_day else "\033[1;30m"
n_color = "\033[1;32m" if in_night else "\033[1;30m"
row_highlight = "\033[1;44m" if idx == selected_idx else ""
max_filename_length = term_width - 15
display_file = file
if len(file) > max_filename_length:
display_file = file[:max_filename_length-3] + "..."
self.terminal.move_cursor(line_row)
self.terminal.clear_line()
if not self.config.is_custom_mode: print(f"{row_highlight}[{l_color}L\033[0m{row_highlight}] [{m_color}M\033[0m{row_highlight}] [{d_color}D\033[0m{row_highlight}] [{n_color}N\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
else: print(f"{row_highlight}[{d_color}C\033[0m{row_highlight}] {display_file}\033[0m", end="", flush=True)
# Clear remaining lines
for clear_row in range(9 + (end_idx - start_idx), term_height):
self.terminal.move_cursor(clear_row)
self.terminal.clear_line()
if state:
state.last_files_display = files_display_state
state.last_selected_idx = selected_idx
class Application: class Application:
def __init__(self, config: Config): def __init__(self, config: Config):
@@ -633,8 +659,8 @@ class Application:
self.in_search_mode = False self.in_search_mode = False
# Data # Data
self.all_audio_files = [] self.all_file_items = []
self.filtered_files = [] self.filtered_file_items = []
self.playlists = {} self.playlists = {}
self.days_of_week = [] self.days_of_week = []
@@ -649,12 +675,12 @@ class Application:
def initialize_data(self): def initialize_data(self):
"""Initialize application data.""" """Initialize application data."""
self.all_audio_files = self.file_manager.get_audio_files(FILES_DIR) self.all_file_items = self.file_manager.get_file_items(FILES_DIR)
if not self.all_audio_files: if not self.all_file_items:
print("No audio files found. Exiting.") print("No audio files or folders found. Exiting.")
return False return False
self.filtered_files = self.all_audio_files.copy() self.filtered_file_items = self.all_file_items.copy()
if self.config.is_custom_mode: if self.config.is_custom_mode:
self.days_of_week = ["custom"] # Single "day" for custom mode self.days_of_week = ["custom"] # Single "day" for custom mode
@@ -665,18 +691,18 @@ class Application:
return True return True
def update_search(self, new_search: str): def update_search(self, new_search: str):
"""Update search term and filter files.""" """Update search term and filter file items."""
self.search_term = new_search self.search_term = new_search
self.filtered_files = self.search_manager.filter_files(self.all_audio_files, self.search_term) self.filtered_file_items = self.search_manager.filter_file_items(self.all_file_items, self.search_term)
# Reset selection if current selection is not in filtered results # Reset selection if current selection is not in filtered results
if self.selected_idx >= len(self.filtered_files): if self.selected_idx >= len(self.filtered_file_items):
self.selected_idx = max(0, len(self.filtered_files) - 1) self.selected_idx = max(0, len(self.filtered_file_items) - 1)
elif self.filtered_files and self.selected_idx < len(self.filtered_files): elif self.filtered_file_items and self.selected_idx < len(self.filtered_file_items):
# Keep current file selected if it's still in results # Keep current item selected if it's still in results
current_file = self.all_audio_files[self.selected_idx] if self.selected_idx < len(self.all_audio_files) else None current_item = self.all_file_items[self.selected_idx] if self.selected_idx < len(self.all_file_items) else None
if current_file and current_file in self.filtered_files: if current_item and current_item in self.filtered_file_items:
self.selected_idx = self.filtered_files.index(current_file) self.selected_idx = self.filtered_file_items.index(current_item)
else: else:
self.selected_idx = 0 self.selected_idx = 0
@@ -703,20 +729,20 @@ class Application:
if self.config.is_custom_mode: if self.config.is_custom_mode:
print("UP/DOWN: Navigate | C: Toggle | /: Search | Q: Quit", end="", flush=True) print("UP/DOWN: Navigate | C: Toggle | /: Search | Q: Quit", end="", flush=True)
else: else:
print("UP/DOWN: Navigate | D/N/L/M: Toggle | C: Copy day | F: Copy file | /: Search | Q: Quit", end="", flush=True) print("UP/DOWN: Navigate | D/N/L/M: Toggle | C: Copy day | F: Copy item | /: Search | Q: Quit", end="", flush=True)
self.terminal.move_cursor(keybind_row + 1) self.terminal.move_cursor(keybind_row + 1)
print("ESC: Exit search | ENTER: Apply search", end="", flush=True) print("ESC: Exit search | ENTER: Apply search", end="", flush=True)
# Draw header # Draw header
self.display.draw_header(self.playlists, current_day, self.current_day_idx, self.display.draw_header(self.playlists, current_day, self.current_day_idx,
self.days_of_week, term_width, force_redraw, self.state) self.days_of_week, term_width, self.all_file_items, force_redraw, self.state)
# Draw search bar # Draw search bar
self.display.draw_search_bar(self.search_term, term_width, force_redraw, self.state) self.display.draw_search_bar(self.search_term, term_width, force_redraw, self.state)
# Draw files section # Draw files section
self.display.draw_files_section(self.filtered_files, self.playlists, self.selected_idx, self.display.draw_files_section(self.filtered_file_items, self.playlists, self.selected_idx,
current_day, self.scroll_offset, term_width, term_height, current_day, self.scroll_offset, term_width, term_height,
force_redraw, self.state) force_redraw, self.state)
@@ -741,7 +767,7 @@ class Application:
if key == 'A': # Up arrow if key == 'A': # Up arrow
self.selected_idx = max(0, self.selected_idx - 1) self.selected_idx = max(0, self.selected_idx - 1)
elif key == 'B': # Down arrow elif key == 'B': # Down arrow
self.selected_idx = min(len(self.filtered_files) - 1, self.selected_idx + 1) self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + 1)
elif key == 'C' and not self.config.is_custom_mode: # Right arrow (disabled in custom mode) elif key == 'C' and not self.config.is_custom_mode: # Right arrow (disabled in custom mode)
self.current_day_idx = (self.current_day_idx + 1) % len(self.days_of_week) self.current_day_idx = (self.current_day_idx + 1) % len(self.days_of_week)
elif key == 'D' and not self.config.is_custom_mode: # Left arrow (disabled in custom mode) elif key == 'D' and not self.config.is_custom_mode: # Left arrow (disabled in custom mode)
@@ -749,40 +775,46 @@ class Application:
elif key == '5': # Page Up elif key == '5': # Page Up
self.selected_idx = max(0, self.selected_idx - visible_lines) self.selected_idx = max(0, self.selected_idx - visible_lines)
elif key == '6': # Page Down elif key == '6': # Page Down
self.selected_idx = min(len(self.filtered_files) - 1, self.selected_idx + visible_lines) self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + visible_lines)
elif key == '1': # Home elif key == '1': # Home
self.selected_idx = 0 self.selected_idx = 0
elif key == '4': # End elif key == '4': # End
self.selected_idx = len(self.filtered_files) - 1 self.selected_idx = len(self.filtered_file_items) - 1
def toggle_playlist(self, period: str): def toggle_playlist(self, period: str):
"""Toggle current file in specified playlist period.""" """Toggle current file item in specified playlist period."""
if not self.filtered_files: if not self.filtered_file_items:
return return
current_day = self.days_of_week[self.current_day_idx] current_day = self.days_of_week[self.current_day_idx]
file = self.filtered_files[self.selected_idx] file_item = self.filtered_file_items[self.selected_idx]
if self.config.is_custom_mode: if self.config.is_custom_mode:
# In custom mode, all operations work with the "day" period # In custom mode, all operations work with the "day" period
is_in_playlist = file in self.playlists["custom"]["day"] is_in_playlist = self.playlist_manager.is_file_item_in_playlist(file_item, "custom", "day", self.playlists)
if is_in_playlist: if is_in_playlist:
self.playlists["custom"]["day"].remove(file) # Remove all files from the item
for filepath in file_item.all_files:
self.playlists["custom"]["day"].discard(filepath)
else: else:
self.playlists["custom"]["day"].add(file) # Add all files from the item
self.playlists["custom"]["day"].update(file_item.all_files)
self.playlist_manager.update_playlist_file("custom", "day", file, not is_in_playlist) self.playlist_manager.update_playlist_file("custom", "day", file_item, not is_in_playlist)
else: else:
# Original weekly mode # Original weekly mode
is_in_playlist = file in self.playlists[current_day][period] is_in_playlist = self.playlist_manager.is_file_item_in_playlist(file_item, current_day, period, self.playlists)
if is_in_playlist: if is_in_playlist:
self.playlists[current_day][period].remove(file) # Remove all files from the item
for filepath in file_item.all_files:
self.playlists[current_day][period].discard(filepath)
else: else:
self.playlists[current_day][period].add(file) # Add all files from the item
self.playlists[current_day][period].update(file_item.all_files)
self.playlist_manager.update_playlist_file(current_day, period, file, not is_in_playlist) self.playlist_manager.update_playlist_file(current_day, period, file_item, not is_in_playlist)
def handle_search_input(self, key: str): def handle_search_input(self, key: str):
"""Handle search input.""" """Handle search input."""
@@ -872,7 +904,7 @@ class Application:
elif key == ' ': elif key == ' ':
header_height = self.display.get_header_height() header_height = self.display.get_header_height()
visible_lines = term_height - (header_height + 5) visible_lines = term_height - (header_height + 5)
self.selected_idx = min(len(self.filtered_files) - 1, self.selected_idx + visible_lines) self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + visible_lines)
elif key.lower() == 'c': elif key.lower() == 'c':
if self.config.is_custom_mode: if self.config.is_custom_mode:
# In custom mode, 'c' toggles the custom playlist # In custom mode, 'c' toggles the custom playlist
@@ -893,29 +925,30 @@ class Application:
elif key.lower() == 'l' and not self.config.is_custom_mode: elif key.lower() == 'l' and not self.config.is_custom_mode:
self.toggle_playlist('late_night') self.toggle_playlist('late_night')
elif key.lower() == 'f' and not self.config.is_custom_mode: elif key.lower() == 'f' and not self.config.is_custom_mode:
if self.filtered_files: if self.filtered_file_items:
current_day = self.days_of_week[self.current_day_idx] current_day = self.days_of_week[self.current_day_idx]
current_file = self.filtered_files[self.selected_idx] current_item = self.filtered_file_items[self.selected_idx]
self.playlists, success = self.playlist_manager.copy_current_file_to_all( self.playlists, success = self.playlist_manager.copy_current_item_to_all(
self.playlists, current_day, self.days_of_week, current_file) self.playlists, current_day, self.days_of_week, current_item)
if success: if success:
self.flash_message = f"File '{current_file}' copied to all days!" item_name = current_item.display_name
self.flash_message = f"Item '{item_name}' copied to all days!"
else: else:
self.flash_message = f"File not in any playlist! Add it first." self.flash_message = f"Item not in any playlist! Add it first."
self.message_timer = 0 self.message_timer = 0
elif key.isupper() and len(key) == 1 and key.isalpha(): elif key.isupper() and len(key) == 1 and key.isalpha():
# Jump to file starting with letter # Jump to item starting with letter
target_letter = key.lower() target_letter = key.lower()
found_idx = -1 found_idx = -1
for i in range(self.selected_idx + 1, len(self.filtered_files)): for i in range(self.selected_idx + 1, len(self.filtered_file_items)):
if self.filtered_files[i].lower().startswith(target_letter): if self.filtered_file_items[i].name.lower().startswith(target_letter):
found_idx = i found_idx = i
break break
if found_idx == -1: if found_idx == -1:
for i in range(0, self.selected_idx): for i in range(0, self.selected_idx):
if self.filtered_files[i].lower().startswith(target_letter): if self.filtered_file_items[i].name.lower().startswith(target_letter):
found_idx = i found_idx = i
break break
if found_idx != -1: if found_idx != -1: