idk if this works

This commit is contained in:
2025-11-15 21:12:56 +01:00
parent 2966285aa3
commit 760f13fabe
+212 -160
View File
@@ -10,11 +10,12 @@ import libcache
import argparse
from datetime import datetime
from typing import List, Dict, Set, Tuple, Optional
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
# Configuration
FILES_DIR = "/home/user/mixes/"
PLAYLISTS_DIR = "/home/user/playlists/"
FILES_DIR = Path("/home/user/mixes/")
PLAYLISTS_DIR = Path("/home/user/playlists/")
POLISH_INDICATORS = ("Polskie", "Dzem")
@dataclass
@@ -39,8 +40,9 @@ class Config:
class FileItem:
"""Represents either a single file or a folder containing files."""
name: str
path: Path
is_folder: bool
files: List[str]
_all_files_cache: Optional[Set[str]] = field(default=None, init=False, repr=False)
@property
def display_name(self) -> str:
@@ -49,52 +51,43 @@ class FileItem:
return self.name
@property
def all_files(self) -> List[str]:
if self.is_folder: return [os.path.join(self.name, f) for f in self.files]
return self.files
def all_files(self) -> Set[str]:
"""Return set of relative paths for all files in this item."""
if self._all_files_cache is not None:
return self._all_files_cache
if self.is_folder:
# For folders, get all files inside
folder_path = self.path.parent
files = set()
if folder_path.exists():
for file in sorted(folder_path.glob("*")):
if file.is_file():
rel_path = str(file.relative_to(FILES_DIR))
files.add(rel_path)
self._all_files_cache = files
return files
else:
# For single files
rel_path = str(self.path.relative_to(FILES_DIR))
self._all_files_cache = {rel_path}
return self._all_files_cache
class FileManager:
@staticmethod
def get_audio_files(directory: str) -> List[str]:
"""Get all audio files from the specified directory (legacy method for compatibility)."""
audio_files = []
try:
for file in os.listdir(directory):
audio_files.append(file)
return sorted(audio_files)
except FileNotFoundError:
print(f"Error: Directory '{directory}' not found.")
return []
except PermissionError:
print(f"Error: Permission denied for directory '{directory}'.")
return []
@staticmethod
def get_file_items(directory: str) -> List[FileItem]:
def get_file_items(directory: Path) -> 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):
# 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):
audio_files.append(file)
except (PermissionError, FileNotFoundError): continue
if audio_files:
# Folder contains audio files
items.append(FileItem(name=entry, is_folder=True, files=["*"]))
for entry in sorted(directory.glob("*")):
if entry.is_dir():
# Create folder item
item = FileItem(name=entry.name, path=entry / "*", is_folder=True)
items.append(item)
elif entry.is_file():
# Create file item
item = FileItem(name=entry.name, path=entry, is_folder=False)
items.append(item)
return items
except FileNotFoundError:
print(f"Error: Directory '{directory}' not found.")
@@ -106,7 +99,8 @@ class FileManager:
class SearchManager:
@staticmethod
def filter_file_items(items: List[FileItem], search_term: str) -> List[FileItem]:
if not search_term: return items
if not search_term:
return items
search_lower = search_term.lower()
@@ -118,9 +112,12 @@ class SearchManager:
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)
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 starts_with + contains + has_chars
@@ -131,7 +128,6 @@ class SearchManager:
search_lower = search_term.lower()
# Group files by match type
starts_with = []
contains = []
has_chars = []
@@ -139,9 +135,12 @@ class SearchManager:
for file in files:
file_lower = file.lower()
if file_lower.startswith(search_lower): starts_with.append(file)
elif search_lower in file_lower: contains.append(file)
elif SearchManager._has_matching_chars(file_lower, search_lower): has_chars.append(file)
if file_lower.startswith(search_lower):
starts_with.append(file)
elif search_lower in file_lower:
contains.append(file)
elif SearchManager._has_matching_chars(file_lower, search_lower):
has_chars.append(file)
return starts_with + contains + has_chars
@@ -158,10 +157,10 @@ class PlaylistManager:
self.config = config
self.custom_playlist_files = set()
def ensure_playlist_dir(self, day: str) -> str:
def ensure_playlist_dir(self, day: str) -> Path:
"""Ensure playlist directory exists for the given day."""
playlist_dir = os.path.expanduser(os.path.join(PLAYLISTS_DIR, day))
if not os.path.exists(playlist_dir): os.makedirs(playlist_dir)
playlist_dir = PLAYLISTS_DIR / day
playlist_dir.mkdir(parents=True, exist_ok=True)
return playlist_dir
def load_playlists(self, days: List[str]) -> Dict[str, Dict[str, Set[str]]]:
@@ -170,15 +169,21 @@ class PlaylistManager:
# In custom mode, we only need one "day" entry
playlists = {"custom": {period: set() for period in self.periods}}
# Load existing custom playlist if it exists
if os.path.exists(self.config.custom_playlist_file):
with open(self.config.custom_playlist_file, 'r') as f:
custom_path = Path(self.config.custom_playlist_file)
if custom_path.exists():
with open(custom_path, 'r') as f:
for line in f:
line = line.strip()
if line:
# Store relative path for comparison
rel_path = os.path.relpath(line, FILES_DIR) if line.startswith('/') else line
# Convert to relative path
abs_path = Path(line)
try:
rel_path = str(abs_path.relative_to(FILES_DIR))
except ValueError:
# If it's already relative, use as is
rel_path = line
self.custom_playlist_files.add(rel_path)
# In custom mode, we'll use 'day' as the default period for display
playlists["custom"]["day"].add(rel_path)
return playlists
else:
@@ -186,18 +191,23 @@ class PlaylistManager:
playlists = {}
for day in days:
playlists[day] = {period: set() for period in self.periods}
playlist_dir = os.path.expanduser(os.path.join(PLAYLISTS_DIR, day))
playlist_dir = PLAYLISTS_DIR / day
if os.path.exists(playlist_dir):
if playlist_dir.exists():
for period in self.periods:
playlist_file = os.path.join(playlist_dir, period)
if os.path.exists(playlist_file):
playlist_file = playlist_dir / period
if playlist_file.exists():
with open(playlist_file, 'r') as f:
for line in f:
line = line.strip()
if line:
# Store relative path for comparison
rel_path = os.path.relpath(line, FILES_DIR) if line.startswith('/') else line
# Convert to relative path
abs_path = Path(line)
try:
rel_path = str(abs_path.relative_to(FILES_DIR))
except ValueError:
# If it's already relative, use as is
rel_path = line
playlists[day][period].add(rel_path)
return playlists
@@ -210,58 +220,77 @@ class PlaylistManager:
def _update_custom_playlist(self, file_item: FileItem, add: bool):
"""Update the custom playlist file."""
if not self.config.custom_playlist_file: raise Exception
# Ensure the directory exists
os.makedirs(os.path.dirname(self.config.custom_playlist_file), exist_ok=True)
if not self.config.custom_playlist_file:
raise Exception("No custom playlist file specified")
custom_path = Path(self.config.custom_playlist_file)
custom_path.parent.mkdir(parents=True, exist_ok=True)
# Read existing content
lines = []
if os.path.exists(self.config.custom_playlist_file):
with open(self.config.custom_playlist_file, 'r') as f:
if custom_path.exists():
with open(custom_path, 'r') as f:
lines = f.read().splitlines()
# Get full paths for all files in the item
full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in file_item.all_files]
# Get all files in this item as absolute paths
files_to_process = set()
for rel_path in file_item.all_files:
abs_path = str(FILES_DIR / rel_path)
files_to_process.add(abs_path)
if add:
for full_filepath in full_filepaths:
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)
# Add new files that aren't already in the list
for filepath in files_to_process:
if filepath not in lines:
lines.append(filepath)
# Also update the tracking set with relative path
try:
rel_path = str(Path(filepath).relative_to(FILES_DIR))
self.custom_playlist_files.add(rel_path)
except ValueError:
pass
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)
# Remove files
for filepath in files_to_process:
while filepath in lines:
lines.remove(filepath)
# Also update the tracking set
try:
rel_path = str(Path(filepath).relative_to(FILES_DIR))
self.custom_playlist_files.discard(rel_path)
except ValueError:
pass
# Write back to file
with open(self.config.custom_playlist_file, 'w') as f:
with open(custom_path, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else ''))
def _update_weekly_playlist(self, day: str, period: str, file_item: FileItem, add: bool):
"""Update a weekly playlist file (original functionality)."""
playlist_dir = self.ensure_playlist_dir(day)
playlist_file = os.path.join(playlist_dir, period)
playlist_file = playlist_dir / period
if not os.path.exists(playlist_file):
with open(playlist_file, 'w') as f: pass
if not playlist_file.exists():
playlist_file.touch()
with open(playlist_file, 'r') as f: lines = f.read().splitlines()
with open(playlist_file, 'r') as f:
lines = f.read().splitlines()
# Get full paths for all files in the item
full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in file_item.all_files]
# Get all files in this item as absolute paths
files_to_process = set()
for rel_path in file_item.all_files:
abs_path = str(FILES_DIR / rel_path)
files_to_process.add(abs_path)
if add:
for full_filepath in full_filepaths:
if full_filepath not in lines:
lines.append(full_filepath)
# Add new files that aren't already in the list
for filepath in files_to_process:
if filepath not in lines:
lines.append(filepath)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
# Remove files
for filepath in files_to_process:
while filepath in lines:
lines.remove(filepath)
with open(playlist_file, 'w', encoding='utf-8', errors='strict') as f:
for line in lines:
@@ -273,11 +302,9 @@ class PlaylistManager:
exit()
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
"""Check if ALL files in the item are in the playlist."""
playlist_set = playlists.get(day, {}).get(period, set())
return all(filepath in playlist_set for filepath in file_item.all_files)
return all(rel_path in playlist_set for rel_path in file_item.all_files)
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."""
@@ -290,10 +317,11 @@ class PlaylistManager:
for period in self.periods:
target_dir = self.ensure_playlist_dir(target_day)
target_file = os.path.join(target_dir, period)
target_file = target_dir / period
filepaths = [os.path.join(FILES_DIR, filename)
for filename in playlists[source_day][period]]
# Convert relative paths to absolute paths
filepaths = [str(FILES_DIR / rel_path)
for rel_path in playlists[source_day][period]]
with open(target_file, 'w') as f:
f.write('\n'.join(filepaths) + ('\n' if filepaths else ''))
@@ -321,34 +349,38 @@ class PlaylistManager:
for period, is_present in source_periods.items():
target_set = playlists[target_day][period]
# Get all relative paths for this item
item_rel_paths = current_item.all_files
if is_present:
# Add all files from the item
target_set.update(current_item.all_files)
target_set.update(item_rel_paths)
else:
# Remove all files from the item
for filepath in current_item.all_files:
target_set.discard(filepath)
for rel_path in item_rel_paths:
target_set.discard(rel_path)
# Update the playlist file
playlist_dir = self.ensure_playlist_dir(target_day)
playlist_file = os.path.join(playlist_dir, period)
playlist_file = playlist_dir / period
if os.path.exists(playlist_file):
if playlist_file.exists():
with open(playlist_file, 'r') as f:
lines = [line.strip() for line in f.readlines()]
else:
lines = []
full_filepaths = [os.path.join(FILES_DIR, filepath) for filepath in current_item.all_files]
# Convert relative paths to absolute for file storage
abs_paths = [str(FILES_DIR / rel_path) for rel_path in item_rel_paths]
if is_present:
for full_filepath in full_filepaths:
if full_filepath not in lines:
lines.append(full_filepath)
for abs_path in abs_paths:
if abs_path not in lines:
lines.append(abs_path)
else:
for full_filepath in full_filepaths:
while full_filepath in lines:
lines.remove(full_filepath)
for abs_path in abs_paths:
while abs_path in lines:
lines.remove(abs_path)
with open(playlist_file, 'w') as f:
f.write('\n'.join(lines) + ('\n' if lines else ''))
@@ -360,12 +392,12 @@ class TerminalUtils:
def get_char() -> str:
"""Get a single character from stdin."""
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd) # type: ignore
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno()) # type: ignore
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) # type: ignore
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
@staticmethod
@@ -409,7 +441,8 @@ class DisplayManager:
days: List[str], term_width: int,
force_redraw: bool = False, state: InterfaceState | None = None):
"""Draw the header, only if content has changed."""
if not state: raise Exception
if not state:
raise Exception("State required")
if self.config.is_custom_mode:
# Custom mode header
@@ -428,7 +461,8 @@ class DisplayManager:
def draw_search_bar(self, search_term: str, force_redraw: bool = False,
state: InterfaceState | None = None):
"""Draw the search bar, only if the search term has changed."""
if not state: raise Exception
if not state:
raise Exception("State required")
# Optimization: Only redraw if search term changes
if force_redraw or state.last_search != search_term:
self.terminal.move_cursor(4)
@@ -441,7 +475,8 @@ class DisplayManager:
current_day: str, scroll_offset: int, term_width: int, term_height: int,
force_redraw: bool = False, state: InterfaceState | None = None):
"""Draw the files list, optimized to only redraw when necessary."""
if not state: raise Exception
if not state:
raise Exception("State required")
available_lines = term_height - 6
start_idx = scroll_offset
@@ -460,8 +495,10 @@ class DisplayManager:
self.terminal.move_cursor(6)
self.terminal.clear_line()
if start_idx > 0: print("", end="")
else: print(" ", end="")
if start_idx > 0:
print("", end="")
else:
print(" ", end="")
if self.config.is_custom_mode:
position_info = f" Custom | Item {selected_idx + 1}/{len(file_items)} "
@@ -484,8 +521,8 @@ class DisplayManager:
if self.config.is_custom_mode:
# In custom mode, only show 'C' for custom playlist
in_custom = all(filepath in playlists.get("custom", {}).get("day", set())
for filepath in item.all_files)
in_custom = all(rel_path in playlists.get("custom", {}).get("day", set())
for rel_path in item.all_files)
c_color = "\033[1;32m" if in_custom else "\033[1;30m"
row_highlight = "\033[1;44m" if idx == selected_idx else ""
@@ -497,14 +534,14 @@ class DisplayManager:
print(f"{row_highlight}[{c_color}C\033[0m{row_highlight}] {display_name}\033[0m", end="", flush=True)
else:
# Original weekly mode display
in_late_night = all(filepath in playlists[current_day]['late_night']
for filepath in item.all_files)
in_morning = all(filepath in playlists[current_day]['morning']
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)
in_late_night = all(rel_path in playlists[current_day]['late_night']
for rel_path in item.all_files)
in_morning = all(rel_path in playlists[current_day]['morning']
for rel_path in item.all_files)
in_day = all(rel_path in playlists[current_day]['day']
for rel_path in item.all_files)
in_night = all(rel_path in playlists[current_day]['night']
for rel_path in item.all_files)
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"
@@ -532,14 +569,14 @@ class DisplayManager:
def _get_item_playlist_status(self, item: FileItem, playlists: Dict, current_day: str) -> Tuple:
"""Get playlist status for an item to use in display state comparison."""
if self.config.is_custom_mode:
return (all(filepath in playlists.get("custom", {}).get("day", set())
for filepath in item.all_files),)
return (all(rel_path in playlists.get("custom", {}).get("day", set())
for rel_path in item.all_files),)
else:
return (
all(filepath in playlists[current_day]['late_night'] for filepath in item.all_files),
all(filepath in playlists[current_day]['morning'] for filepath in item.all_files),
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)
all(rel_path in playlists[current_day]['late_night'] for rel_path in item.all_files),
all(rel_path in playlists[current_day]['morning'] for rel_path in item.all_files),
all(rel_path in playlists[current_day]['day'] for rel_path in item.all_files),
all(rel_path in playlists[current_day]['night'] for rel_path in item.all_files)
)
class Application:
@@ -695,8 +732,8 @@ class Application:
if is_in_playlist:
# Remove all files from the item
for filepath in file_item.all_files:
self.playlists["custom"]["day"].discard(filepath)
for rel_path in file_item.all_files:
self.playlists["custom"]["day"].discard(rel_path)
else:
# Add all files from the item
self.playlists["custom"]["day"].update(file_item.all_files)
@@ -708,8 +745,8 @@ class Application:
if is_in_playlist:
# Remove all files from the item
for filepath in file_item.all_files:
self.playlists[current_day][period].discard(filepath)
for rel_path in file_item.all_files:
self.playlists[current_day][period].discard(rel_path)
else:
# Add all files from the item
self.playlists[current_day][period].update(file_item.all_files)
@@ -767,6 +804,7 @@ class Application:
if needs_redraw:
self.draw_interface()
self.state.last_selected_idx = self.selected_idx
self.state.last_current_day_idx = self.current_day_idx
self.state.last_scroll_offset = self.scroll_offset
@@ -786,16 +824,20 @@ class Application:
continue
# Handle regular input
if key == 'q': break
elif key == '/': self.in_search_mode = True
if key == 'q':
break
elif key == '/':
self.in_search_mode = True
elif key == '\x1b': # Escape sequences
next_key = self.terminal.get_char()
if next_key == '[':
arrow_key = self.terminal.get_char()
self.handle_navigation_key(arrow_key)
if arrow_key in ['5', '6', '1', '4']:
try: self.terminal.get_char() # Consume the ~ character
except: pass
try:
self.terminal.get_char() # Consume the ~ character
except:
pass
elif key == ' ':
self.selected_idx = min(len(self.filtered_file_items) - 1, self.selected_idx + (term_height - 6))
elif key.lower() == 'c':
@@ -808,10 +850,14 @@ class Application:
self.playlists = self.playlist_manager.copy_day_to_all(self.playlists, current_day, self.days_of_week)
self.flash_message = f"Playlists from {current_day} copied to all other days!"
self.message_timer = 0
elif key.lower() == 'm' and not self.config.is_custom_mode: self.toggle_playlist('morning')
elif key.lower() == 'd' and not self.config.is_custom_mode: self.toggle_playlist('day')
elif key.lower() == 'n' and not self.config.is_custom_mode: self.toggle_playlist('night')
elif key.lower() == 'l' and not self.config.is_custom_mode: self.toggle_playlist('late_night')
elif key.lower() == 'm' and not self.config.is_custom_mode:
self.toggle_playlist('morning')
elif key.lower() == 'd' and not self.config.is_custom_mode:
self.toggle_playlist('day')
elif key.lower() == 'n' and not self.config.is_custom_mode:
self.toggle_playlist('night')
elif key.lower() == 'l' and not self.config.is_custom_mode:
self.toggle_playlist('late_night')
elif key.lower() == 'f' and not self.config.is_custom_mode:
if self.filtered_file_items:
current_day = self.days_of_week[self.current_day_idx]
@@ -822,7 +868,8 @@ class Application:
if success:
item_name = current_item.display_name
self.flash_message = f"Item '{item_name}' copied to all days!"
else: self.flash_message = f"Item not in any playlist! Add it first."
else:
self.flash_message = f"Item not in any playlist! Add it first."
self.message_timer = 0
elif key.isupper() and len(key) == 1 and key.isalpha():
# Jump to item starting with letter
@@ -837,7 +884,8 @@ class Application:
if self.filtered_file_items[i].name.lower().startswith(target_letter):
found_idx = i
break
if found_idx != -1: self.selected_idx = found_idx
if found_idx != -1:
self.selected_idx = found_idx
finally:
self.terminal.show_cursor()
@@ -865,14 +913,18 @@ def main():
args = parse_arguments()
fd = sys.stdin.fileno()
original_settings = termios.tcgetattr(fd) # type: ignore
original_settings = termios.tcgetattr(fd)
new_settings = termios.tcgetattr(fd) # type: ignore
new_settings[3] = new_settings[3] & ~termios.ECHOCTL # type: ignore
termios.tcsetattr(fd, termios.TCSADRAIN, new_settings) # type: ignore
new_settings = termios.tcgetattr(fd)
new_settings[3] = new_settings[3] & ~termios.ECHOCTL
termios.tcsetattr(fd, termios.TCSADRAIN, new_settings)
config = Config(custom_playlist_file=args.playlist)
app = Application(config)
code = app.run()
termios.tcsetattr(fd, termios.TCSADRAIN, original_settings) # type: ignore
termios.tcsetattr(fd, termios.TCSADRAIN, original_settings)
exit(code)
if __name__ == "__main__":
main()