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