From d4b90436be67e5410802ebfdd01d57fb986ea3eb Mon Sep 17 00:00:00 2001 From: Yonokid <37304577+Yonokid@users.noreply.github.com> Date: Wed, 13 Aug 2025 02:14:02 -0400 Subject: [PATCH] add difficulty sorting menu --- libs/audio.py | 2 +- libs/song_hash.py | 50 ++++-- libs/tja.py | 10 +- libs/utils.py | 1 + scenes/game.py | 21 ++- scenes/song_select.py | 377 +++++++++++++++++++++++++++++++++++------- 6 files changed, 379 insertions(+), 82 deletions(-) diff --git a/libs/audio.py b/libs/audio.py index 30ebd81..0349ed2 100644 --- a/libs/audio.py +++ b/libs/audio.py @@ -828,7 +828,7 @@ class AudioEngine: def set_sound_volume(self, sound: str, volume: float) -> None: if sound in self.sounds: - self.sounds[sound].volume = max(0.0, min(1.0, volume)) + self.sounds[sound].volume = max(0.0, volume) def set_sound_pan(self, sound: str, pan: float) -> None: if sound in self.sounds: diff --git a/libs/song_hash.py b/libs/song_hash.py index 755c884..1f8c12f 100644 --- a/libs/song_hash.py +++ b/libs/song_hash.py @@ -1,5 +1,6 @@ import csv import json +import sqlite3 import sys import time from collections import deque @@ -28,7 +29,6 @@ def build_song_hashes(output_dir=Path("cache")): output_dir.mkdir() song_hashes: dict[str, list[dict]] = dict() path_to_hash: dict[str, str] = dict() # New index for O(1) path lookups - output_path = Path(output_dir / "song_hashes.json") index_path = Path(output_dir / "path_to_hash.json") @@ -36,7 +36,6 @@ def build_song_hashes(output_dir=Path("cache")): if output_path.exists(): with open(output_path, "r", encoding="utf-8") as f: song_hashes = json.load(f, cls=DiffHashesDecoder) - if index_path.exists(): with open(index_path, "r", encoding="utf-8") as f: path_to_hash = json.load(f) @@ -59,21 +58,17 @@ def build_song_hashes(output_dir=Path("cache")): all_tja_files.extend(root_path.rglob("*.tja")) global_data.total_songs = len(all_tja_files) - files_to_process = [] for tja_path in all_tja_files: tja_path_str = str(tja_path) current_modified = tja_path.stat().st_mtime - if current_modified <= saved_timestamp: current_hash = path_to_hash.get(tja_path_str) if current_hash is not None: global_data.song_paths[tja_path] = current_hash continue - current_hash = path_to_hash.get(tja_path_str) - if current_hash is None: files_to_process.append(tja_path) else: @@ -82,15 +77,19 @@ def build_song_hashes(output_dir=Path("cache")): del song_hashes[current_hash] del path_to_hash[tja_path_str] + # Prepare database connection for updates + db_path = Path("scores.db") + db_updates = [] # Store updates to batch process later + # Process only files that need updating song_count = 0 total_songs = len(files_to_process) if total_songs > 0: global_data.total_songs = total_songs + for tja_path in files_to_process: tja_path_str = str(tja_path) current_modified = tja_path.stat().st_mtime - tja = TJAParser(tja_path) all_notes = deque() all_bars = deque() @@ -106,7 +105,6 @@ def build_song_hashes(output_dir=Path("cache")): continue hash_val = tja.hash_note_data(all_notes, all_bars) - if hash_val not in song_hashes: song_hashes[hash_val] = [] @@ -121,16 +119,48 @@ def build_song_hashes(output_dir=Path("cache")): # Update both indexes path_to_hash[tja_path_str] = hash_val global_data.song_paths[tja_path] = hash_val + + # Prepare database updates for each difficulty + en_name = tja.metadata.title.get('en', '') if isinstance(tja.metadata.title, dict) else str(tja.metadata.title) + jp_name = tja.metadata.title.get('jp', '') if isinstance(tja.metadata.title, dict) else '' + + for diff, diff_hash in diff_hashes.items(): + db_updates.append((diff_hash, en_name, jp_name, diff)) + song_count += 1 global_data.song_progress = song_count / total_songs + # Update database with new difficulty hashes + if db_updates and db_path.exists(): + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + for diff_hash, en_name, jp_name, diff in db_updates: + # Update existing entries that match by name and difficulty + cursor.execute(""" + UPDATE scores + SET hash = ? + WHERE (en_name = ? AND jp_name = ?) AND diff = ? + """, (diff_hash, en_name, jp_name, diff)) + if cursor.rowcount > 0: + print(f"Updated {cursor.rowcount} entries for {en_name} ({diff})") + + conn.commit() + conn.close() + print(f"Database update completed. Processed {len(db_updates)} difficulty hash updates.") + + except sqlite3.Error as e: + print(f"Database error: {e}") + except Exception as e: + print(f"Error updating database: {e}") + elif db_updates: + print(f"Warning: scores.db not found, skipping {len(db_updates)} database updates") + # Save both files with open(output_path, "w", encoding="utf-8") as f: json.dump(song_hashes, f, indent=2, ensure_ascii=False) - with open(index_path, "w", encoding="utf-8") as f: json.dump(path_to_hash, f, indent=2, ensure_ascii=False) - with open(output_dir / 'timestamp.txt', 'w') as f: f.write(str(current_timestamp)) diff --git a/libs/tja.py b/libs/tja.py index ef1809b..6b453e3 100644 --- a/libs/tja.py +++ b/libs/tja.py @@ -40,11 +40,13 @@ class Note: return self.hit_ms == other.hit_ms def _get_hash_data(self) -> bytes: - """Get deterministic byte representation for hashing""" + hash_fields = ['type', 'hit_ms', 'load_ms'] field_values = [] - for f in sorted([f.name for f in fields(self)]): - value = getattr(self, f, None) - field_values.append((f, value)) + + for field_name in sorted(hash_fields): + value = getattr(self, field_name, None) + field_values.append((field_name, value)) + field_values.append(('__class__', self.__class__.__name__)) hash_string = str(field_values) return hash_string.encode('utf-8') diff --git a/libs/utils.py b/libs/utils.py index 26bbc2b..660c89a 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -240,6 +240,7 @@ for file in Path('cache/image').iterdir(): class OutlinedText: def __init__(self, text: str, font_size: int, color: ray.Color, outline_color: ray.Color, outline_thickness=5.0, vertical=False): + self.text = text self.hash = self._hash_text(text, font_size, color, vertical) if self.hash in text_cache: self.texture = ray.load_texture(f'cache/image/{self.hash}.png') diff --git a/scenes/game.py b/scenes/game.py index 461efed..2222daa 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -60,8 +60,8 @@ class GameScreen: session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) if not hasattr(self, 'song_music'): if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file(): - self.song_music = audio.load_sound(self.tja.metadata.wave) - audio.normalize_sound(self.song_music, 0.1935) + self.song_music = audio.load_music_stream(self.tja.metadata.wave) + audio.normalize_music_stream(self.song_music, 0.1935) else: self.song_music = None self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) @@ -88,7 +88,7 @@ class GameScreen: self.screen_init = False tex.unload_textures() if self.song_music is not None: - audio.unload_sound(self.song_music) + audio.unload_music_stream(self.song_music) del self.song_music self.song_started = False self.end_ms = 0 @@ -132,8 +132,8 @@ class GameScreen: if self.tja is not None: if (self.current_ms >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["judge_offset"]) and not self.song_started: if self.song_music is not None: - if not audio.is_sound_playing(self.song_music): - audio.play_sound(self.song_music) + if not audio.is_music_stream_playing(self.song_music): + audio.play_music_stream(self.song_music) print(f"Song started at {self.current_ms}") if self.movie is not None: self.movie.start(get_current_ms()) @@ -146,6 +146,9 @@ class GameScreen: self.player_1.update(self) self.song_info.update(get_current_ms()) + if self.song_music is not None: + audio.update_music_stream(self.song_music) + self.result_transition.update(get_current_ms()) if self.result_transition.is_finished: return self.on_screen_end('RESULT') @@ -162,13 +165,15 @@ class GameScreen: self.end_ms = get_current_ms() if ray.is_key_pressed(ray.KeyboardKey.KEY_F1): - audio.stop_sound(self.song_music) + if self.song_music is not None: + audio.stop_music_stream(self.song_music) self.init_tja(global_data.selected_song, session_data.selected_difficulty) audio.play_sound(self.sound_restart) self.song_started = False if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): - audio.stop_sound(self.song_music) + if self.song_music is not None: + audio.stop_music_stream(self.song_music) return self.on_screen_end('SONG_SELECT') def draw(self): @@ -613,7 +618,7 @@ class Player: self.draw_balloon(game_screen, note, current_eighth) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) else: - tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192) + tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192, center=True) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) def draw(self, game_screen: GameScreen): diff --git a/scenes/song_select.py b/scenes/song_select.py index 54e686f..299af6f 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -1,3 +1,4 @@ +import random import sqlite3 from datetime import datetime, timedelta from pathlib import Path @@ -25,6 +26,7 @@ from libs.utils import ( class State: BROWSING = 0 SONG_SELECTED = 1 + DIFF_SORTING = 2 class SongSelectScreen: BOX_CENTER = 444 @@ -43,37 +45,43 @@ class SongSelectScreen: self.sound_kat = audio.load_sound(sounds_dir / "inst_00_katsu.wav") self.sound_skip = audio.load_sound(sounds_dir / 'song_select' / 'Skip.ogg') self.sound_ura_switch = audio.load_sound(sounds_dir / 'song_select' / 'SE_SELECT [4].ogg') + self.sound_add_favorite = audio.load_sound(sounds_dir / 'song_select' / 'add_favorite.ogg') audio.set_sound_volume(self.sound_ura_switch, 0.25) + audio.set_sound_volume(self.sound_add_favorite, 3.0) self.sound_bgm = audio.load_sound(sounds_dir / "song_select" / "JINGLE_GENRE [1].ogg") def on_screen_start(self): if not self.screen_init: tex.load_screen_textures('song_select') self.load_sounds() - self.selected_song = None - self.selected_difficulty = -1 self.background_move = tex.get_animation(0) - self.background_move.start() self.move_away = tex.get_animation(1) self.diff_fade_out = tex.get_animation(2) - self.state = State.BROWSING self.text_fade_out = tex.get_animation(3) self.text_fade_in = tex.get_animation(4) self.background_fade_change = tex.get_animation(5) + self.background_move.start() + self.state = State.BROWSING + self.selected_difficulty = -1 + self.selected_song = None self.game_transition = None + self.demo_song = None + self.diff_sort_selector = None self.texture_index = 9 self.last_texture_index = 9 - self.demo_song = None - self.navigator.reset_items() - self.navigator.get_current_item().box.get_scores() - self.screen_init = True self.last_moved = get_current_ms() self.ura_toggle = 0 - self.ura_switch_animation = UraSwitchAnimation() self.is_ura = False + self.screen_init = True + self.ura_switch_animation = UraSwitchAnimation() + if str(global_data.selected_song) in self.navigator.all_song_files: self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)]) + self.navigator.reset_items() + self.navigator.get_current_item().box.get_scores() + self.navigator.add_recent() + def on_screen_end(self, next_screen): self.screen_init = False global_data.selected_song = self.navigator.get_current_item().path @@ -124,6 +132,9 @@ class SongSelectScreen: if selected_item is not None and selected_item.box.is_back: self.navigator.go_back() #audio.play_sound(self.sound_cancel) + elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]: + self.state = State.DIFF_SORTING + self.diff_sort_selector = DiffSortSelect() else: selected_song = self.navigator.select_current_item() if selected_song: @@ -136,6 +147,11 @@ class SongSelectScreen: self.text_fade_out.start() self.text_fade_in.start() + if ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE): + success = self.navigator.add_favorite() + if success: + audio.play_sound(self.sound_add_favorite) + def handle_input_selected(self): # Handle song selection confirmation or cancel if is_l_don_pressed() or is_r_don_pressed(): @@ -164,6 +180,34 @@ class SongSelectScreen: self.selected_difficulty in [3, 4]): self._toggle_ura_mode() + def handle_input_diff_sort(self): + if self.diff_sort_selector is None: + raise Exception("Diff sort selector was not able to be created") + if is_l_kat_pressed(): + self.diff_sort_selector.input_left() + audio.play_sound(self.sound_kat) + if is_r_kat_pressed(): + self.diff_sort_selector.input_right() + audio.play_sound(self.sound_kat) + if is_l_don_pressed() or is_r_don_pressed(): + diff, level = self.diff_sort_selector.input_select() + if diff == -2: + audio.play_sound(self.sound_don) + return + elif diff == -1: + self.diff_sort_selector = None + self.state = State.BROWSING + if level == 0: + audio.play_sound(self.sound_don) + self.navigator.select_current_item() + else: + self.diff_sort_selector = None + self.state = State.BROWSING + audio.play_sound(self.sound_don) + self.navigator.diff_sort_diff = diff + self.navigator.diff_sort_level = level + self.navigator.select_current_item() + def _cancel_selection(self): """Reset to browsing state""" self.selected_song = None @@ -229,6 +273,8 @@ class SongSelectScreen: self.handle_input_browsing() elif self.state == State.SONG_SELECTED: self.handle_input_selected() + elif self.state == State.DIFF_SORTING: + self.handle_input_diff_sort() def update(self): self.on_screen_start() @@ -266,6 +312,9 @@ class SongSelectScreen: if self.navigator.genre_bg is not None: self.navigator.genre_bg.update(get_current_ms()) + if self.diff_sort_selector is not None: + self.diff_sort_selector.update(get_current_ms()) + for song in self.navigator.items: song.box.update(self.state == State.SONG_SELECTED) song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 @@ -311,13 +360,19 @@ class SongSelectScreen: self.ura_switch_animation.draw() - if self.selected_song and self.state == State.SONG_SELECTED: + tex.draw_texture('global', 'footer') + + if self.diff_sort_selector is not None: + self.diff_sort_selector.draw() + + if (self.selected_song and self.state == State.SONG_SELECTED): self.draw_selector() tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) + elif self.state == State.DIFF_SORTING: + tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) else: tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute) - tex.draw_texture('global', 'footer') if self.game_transition is not None: self.game_transition.draw() @@ -335,6 +390,11 @@ class SongBox: 6: ray.Color(134, 88, 0, 255), 7: ray.Color(79, 40, 134, 255), 8: ray.Color(148, 24, 0, 255), + 9: ray.Color(101, 0, 82, 255), + 10: ray.Color(140, 39, 92, 255), + 11: ray.Color(151, 57, 30, 255), + 12: ray.Color(35, 123, 103, 255), + 13: ray.Color(25, 68, 137, 255), 14: ray.Color(157, 13, 31, 255) } def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, @@ -461,11 +521,13 @@ class SongBox: def _draw_closed(self, x: int, y: int): tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) - offset = 1 if self.texture_index == 3 or self.texture_index >= 9 else 0 + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) if self.texture_index == 9: tex.draw_texture('box', 'genre_overlay', x=x, y=y) + elif self.texture_index == 14: + tex.draw_texture('box', 'diff_overlay', x=x, y=y) if not self.is_back and self.is_dir: tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y) @@ -505,12 +567,14 @@ class SongBox: self.hori_name.draw(self.hori_name.default_src, dest, ray.Vector2(0, 0), 0, color) tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute) - offset = 1 if self.texture_index == 3 or self.texture_index >= 9 else 0 + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0 tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=324) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute) if self.texture_index == 9: tex.draw_texture('box', 'genre_overlay_large', x=x, y=y, color=color) + elif self.texture_index == 14: + tex.draw_texture('box', 'diff_overlay_large', x=x, y=y, color=color) color = ray.WHITE if fade_override is not None: color = ray.fade(ray.WHITE, min(0.5, fade_override)) @@ -780,6 +844,122 @@ class UraSwitchAnimation: def draw(self): tex.draw_texture('diff_select', 'ura_switch', frame=self.texture_change.attribute, fade=self.fade_out.attribute) +class DiffSortSelect: + def __init__(self): + self.selected_box = -1 + self.selected_level = 1 + self.in_level_select = False + self.confirmation = False + self.confirm_index = 1 + self.num_boxes = 6 + self.limits = [5, 7, 8, 10] + + def update(self, current_ms): + pass + + def get_random_sort(self): + diff = random.randint(0, 4) + if diff == 0: + level = random.randint(1, 5) + elif diff == 1: + level = random.randint(1, 7) + elif diff == 2: + level = random.randint(1, 8) + elif diff == 3: + level = random.randint(1, 10) + else: + level = random.choice([1, 5, 6, 7, 8, 9, 10]) + return diff, level + + def input_select(self): + if self.confirmation: + if self.confirm_index == 0: + self.confirmation = False + return (-2, -1) + elif self.confirm_index == 1: + return self.selected_box, self.selected_level + elif self.confirm_index == 2: + self.confirmation = False + self.in_level_select = False + return (-2, -1) + elif self.in_level_select: + self.confirmation = True + self.confirm_index = 1 + return (-2, -1) + if self.selected_box == -1: + return (-1, -1) + elif self.selected_box == 5: + return (-1, 0) + elif self.selected_box == 4: + return self.get_random_sort() + self.in_level_select = True + self.selected_level = min(self.selected_level, self.limits[self.selected_box]) + return (-2, -1) + + def input_left(self): + if self.confirmation: + self.confirm_index = max(self.confirm_index - 1, 0) + elif self.in_level_select: + self.selected_level = max(self.selected_level - 1, 1) + else: + self.selected_box = max(self.selected_box - 1, -1) + + def input_right(self): + if self.confirmation: + self.confirm_index = min(self.confirm_index + 1, 2) + elif self.in_level_select: + self.selected_level = min(self.selected_level + 1, self.limits[self.selected_box]) + else: + self.selected_box = min(self.selected_box + 1, self.num_boxes - 1) + + def draw_diff_select(self): + tex.draw_texture('diff_sort', 'background') + + tex.draw_texture('diff_sort', 'back') + for i in range(self.num_boxes): + if i == self.selected_box: + tex.draw_texture('diff_sort', 'box_highlight', x=(100*i)) + tex.draw_texture('diff_sort', 'box_text_highlight', x=(100*i), frame=i) + else: + tex.draw_texture('diff_sort', 'box', x=(100*i)) + tex.draw_texture('diff_sort', 'box_text', x=(100*i), frame=i) + if self.selected_box == -1: + tex.draw_texture('diff_sort', 'back_outline') + else: + tex.draw_texture('diff_sort', 'box_outline', x=(100*self.selected_box)) + + for i in range(self.num_boxes): + if i < 4: + tex.draw_texture('diff_sort', 'box_diff', x=(100*i), frame=i) + + def draw_level_select(self): + tex.draw_texture('diff_sort', 'background') + tex.draw_texture('diff_sort', 'star_select_text') + tex.draw_texture('diff_sort', 'star_limit', frame=self.selected_box) + tex.draw_texture('diff_sort', 'level_box') + tex.draw_texture('diff_sort', 'diff', frame=self.selected_box) + tex.draw_texture('diff_sort', 'star_num', frame=self.selected_level) + for i in range(self.selected_level): + tex.draw_texture('diff_sort', 'star', x=(i*40.5)) + + if self.confirmation: + for i in range(3): + if i == self.confirm_index: + tex.draw_texture('diff_sort', 'small_box_highlight', x=(i*245)) + tex.draw_texture('diff_sort', 'small_box_text_highlight', x=(i*245), frame=i) + else: + tex.draw_texture('diff_sort', 'small_box', x=(i*245)) + tex.draw_texture('diff_sort', 'small_box_text', x=(i*245), frame=i) + else: + tex.draw_texture('diff_sort', 'pongos') + + def draw(self): + ray.draw_rectangle(0, 0, 1280, 720, ray.fade(ray.BLACK, 0.6)) + if self.in_level_select: + self.draw_level_select() + else: + self.draw_diff_select() + class FileSystemItem: GENRE_MAP = { 'J-POP': 1, @@ -790,11 +970,15 @@ class FileSystemItem: 'クラシック': 6, 'ゲームミュージック': 7, 'ナムコオリジナル': 8, + 'RECOMMENDED': 10, + 'FAVORITE': 11, + 'RECENT': 12, 'DIFFICULTY': 14 } """Base class for files and directories in the navigation system""" def __init__(self, path: Path, name: str): self.path = path + self.name = name class Directory(FileSystemItem): """Represents a directory in the navigation system""" @@ -856,7 +1040,12 @@ class FileNavigator: self.current_root_dir = Path() self.items: list[Directory | SongFile] = [] self.new_items: list[Directory | SongFile] = [] + self.favorite_folder: Optional[Directory] = None + self.in_favorites = False + self.recent_folder: Optional[Directory] = None self.selected_index = 0 + self.diff_sort_diff = 4 + self.diff_sort_level = 10 self.history = [] self.box_open = False self.genre_bg = None @@ -915,6 +1104,10 @@ class FileNavigator: box_texture=box_texture, collection=collection ) + if directory_obj.collection == Directory.COLLECTIONS[2]: + self.favorite_folder = directory_obj + elif directory_obj.collection == Directory.COLLECTIONS[1]: + self.recent_folder = directory_obj self.all_directories[dir_key] = directory_obj # Generate content list for this directory @@ -979,7 +1172,7 @@ class FileNavigator: self.root_items.append(self.all_song_files[song_key]) def _count_tja_files(self, folder_path: Path): - """Count TJA files in directory (matching original logic)""" + """Count TJA files in directory""" tja_count = 0 # Find all song_list.txt files recursively @@ -988,16 +1181,10 @@ class FileNavigator: if song_list_files: # Process all song_list.txt files found for song_list_path in song_list_files: - try: - with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file: - tja_count += len([line for line in song_list_file.readlines() if line.strip()]) - except (IOError, UnicodeDecodeError) as e: - # Handle potential file reading errors - print(f"Warning: Could not read {song_list_path}: {e}") - continue - else: - # Fallback: Use recursive counting of .tja files - tja_count = sum(1 for _ in folder_path.rglob("*.tja")) + with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file: + tja_count += len([line for line in song_list_file.readlines() if line.strip()]) + # Fallback: Use recursive counting of .tja files + tja_count += sum(1 for _ in folder_path.rglob("*.tja")) return tja_count @@ -1046,41 +1233,35 @@ class FileNavigator: """Find TJA files only in the specified directory, not recursively in subdirectories with box.def""" tja_files: list[Path] = [] - try: - for path in directory.iterdir(): - if path.is_file() and path.suffix.lower() == ".tja": - tja_files.append(path) - elif path.is_dir(): - # Only recurse into subdirectories that don't have box.def - sub_dir_has_box_def = (path / "box.def").exists() - if not sub_dir_has_box_def: - tja_files.extend(self._find_tja_files_in_directory_only(path)) - except (PermissionError, OSError): - pass + for path in directory.iterdir(): + if path.is_file() and path.suffix.lower() == ".tja": + tja_files.append(path) + elif path.is_dir(): + # Only recurse into subdirectories that don't have box.def + sub_dir_has_box_def = (path / "box.def").exists() + if not sub_dir_has_box_def: + tja_files.extend(self._find_tja_files_in_directory_only(path)) return tja_files def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): tja_files: list[Path] = [] - try: - has_box_def = (directory / "box.def").exists() - # Fixed: Only skip if box_def_dirs_only is True AND has_box_def AND it's not the directory we're currently processing - # During object generation, we want to get files from directories with box.def - if box_def_dirs_only and has_box_def and directory != self.current_dir: - # This logic should only apply during navigation, not during object generation - # During object generation, we want to collect all TJA files - return [] + has_box_def = (directory / "box.def").exists() + # Fixed: Only skip if box_def_dirs_only is True AND has_box_def AND it's not the directory we're currently processing + # During object generation, we want to get files from directories with box.def + if box_def_dirs_only and has_box_def and directory != self.current_dir: + # This logic should only apply during navigation, not during object generation + # During object generation, we want to collect all TJA files + return [] - for path in directory.iterdir(): - if path.is_file() and path.suffix.lower() == ".tja": - tja_files.append(path) - elif path.is_dir(): - sub_dir_has_box_def = (path / "box.def").exists() - if not sub_dir_has_box_def: - tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only)) - except (PermissionError, OSError): - pass + for path in directory.iterdir(): + if path.is_file() and path.suffix.lower() == ".tja": + tja_files.append(path) + elif path.is_dir(): + sub_dir_has_box_def = (path / "box.def").exists() + if not sub_dir_has_box_def: + tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only)) return tja_files @@ -1137,7 +1318,7 @@ class FileNavigator: for i in range(len(value)): song = value[i] if (song["title"]["en"] == title and - song["subtitle"]["en"][2:] == subtitle and + song["subtitle"]["en"] == subtitle and Path(song["file_path"]).exists()): hash_val = key tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"])) @@ -1151,6 +1332,7 @@ class FileNavigator: if file_updated: with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: for line in updated_lines: + print("updated", line) song_list.write(line + '\n') return tja_files @@ -1197,6 +1379,7 @@ class FileNavigator: """Load pre-generated items for the current directory""" has_children = any(item.is_dir() and (item / "box.def").exists() for item in self.current_dir.iterdir()) self.genre_bg = None + self.in_favorites = False if has_children: self.items = [] if not self.box_open: @@ -1222,6 +1405,21 @@ class FileNavigator: if isinstance(selected_item, Directory): if selected_item.collection == Directory.COLLECTIONS[0]: content_items = self.new_items + elif selected_item.collection == Directory.COLLECTIONS[1]: + if self.recent_folder is None: + raise Exception("tried to enter recent folder without recents") + self._generate_objects_recursive(self.recent_folder.path) + selected_item.box.tja_count_text = None + selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path) + content_items = self.directory_contents[dir_key] + elif selected_item.collection == Directory.COLLECTIONS[2]: + if self.favorite_folder is None: + raise Exception("tried to enter favorite folder without favorites") + self._generate_objects_recursive(self.favorite_folder.path) + selected_item.box.tja_count_text = None + selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path) + content_items = self.directory_contents[dir_key] + self.in_favorites = True elif selected_item.collection == Directory.COLLECTIONS[3]: content_items = [] parent_dir = selected_item.path.parent @@ -1230,11 +1428,20 @@ class FileNavigator: sibling_key = str(sibling_path) if sibling_key in self.directory_contents: for item in self.directory_contents[sibling_key]: - if isinstance(item, SongFile): - if 3 in item.tja.metadata.course_data and item.tja.metadata.course_data[3].level == 10: - item.box.texture_index = 14 - content_items.append(item) - + if isinstance(item, SongFile) and item: + if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level: + if item not in content_items: + content_items.append(item) + elif selected_item.collection == Directory.COLLECTIONS[4]: + parent_dir = selected_item.path.parent + temp_items = [] + for sibling_path in parent_dir.iterdir(): + if sibling_path.is_dir() and sibling_path != selected_item.path: + sibling_key = str(sibling_path) + if sibling_key in self.directory_contents: + for item in self.directory_contents[sibling_key]: + temp_items.append(item) + content_items = random.sample(temp_items, 10) i = 1 for item in content_items: if isinstance(item, SongFile): @@ -1242,8 +1449,9 @@ class FileNavigator: back_dir = Directory(self.current_dir.parent, "", 17, back=True) self.items.insert(self.selected_index+i, back_dir) i += 1 - if not has_children: + if selected_item is not None: + item.box.texture_index = selected_item.box.texture_index self.items.insert(self.selected_index+i, item) else: self.items.append(item) @@ -1341,3 +1549,54 @@ class FileNavigator: def reset_items(self): for item in self.items: item.box.reset() + + def add_recent(self): + song = self.get_current_item() + if isinstance(song, Directory): + return + if self.recent_folder is None: + return + + recents_path = self.recent_folder.path / 'song_list.txt' + new_entry = f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n' + existing_entries = [] + if recents_path.exists(): + with open(recents_path, 'r', encoding='utf-8-sig') as song_list: + existing_entries = song_list.readlines() + existing_entries = [entry for entry in existing_entries if not entry.startswith(f'{song.hash}|')] + all_entries = [new_entry] + existing_entries + recent_entries = all_entries[:25] + with open(recents_path, 'w', encoding='utf-8-sig') as song_list: + song_list.writelines(recent_entries) + + print("Added recent: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) + + def add_favorite(self) -> bool: + song = self.get_current_item() + if isinstance(song, Directory): + return False + if self.favorite_folder is None: + return False + favorites_path = self.favorite_folder.path / 'song_list.txt' + lines = [] + with open(favorites_path, 'r', encoding='utf-8-sig') as song_list: + for line in song_list: + line = line.strip() + if not line: # Skip empty lines + continue + hash, title, subtitle = line.split('|') + if song.hash == hash or (song.tja.metadata.title['en'] == title and song.tja.metadata.subtitle['en'] == subtitle): + if not self.in_favorites: + return False + else: + lines.append(line) + if self.in_favorites: + with open(favorites_path, 'w', encoding='utf-8-sig') as song_list: + for line in lines: + song_list.write(line + '\n') + print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) + else: + with open(favorites_path, 'a', encoding='utf-8-sig') as song_list: + song_list.write(f'{song.hash}|{song.tja.metadata.title['en']}|{song.tja.metadata.subtitle['en']}\n') + print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) + return True