diff options
| author | Warry <FunWarry@users.noreply.github.com> | 2025-11-29 12:10:06 +0100 |
|---|---|---|
| committer | Warry <FunWarry@users.noreply.github.com> | 2025-11-29 12:10:06 +0100 |
| commit | d2c3e8ab8f1dcaec2b8be7ff9d1fb6c0df2d5296 (patch) | |
| tree | 2cf51e324cca81e198758e57a1c6e5780627e255 /tidal_dl_ng/gui_search.py | |
| parent | 28a6fe5a7dbadc9532de228a539c47ec49ca4aa2 (diff) | |
| download | tidal-dl-ng-d2c3e8ab8f1dcaec2b8be7ff9d1fb6c0df2d5296.tar.gz | |
feat(hover): add track details preview on hover + fix all ruff violations
Implement comprehensive track information display on hover and resolve all
code quality issues to achieve complete ruff compliance.
MAJOR FEATURES:
- Track details preview on hover with rich metadata display
- Async BPM fetching with loading indicator
- Enhanced metadata utilities with fuzzy matching
- TIDAL API extras integration
FILES MODIFIED: 15+ files (core, UI, tests, docs)
RUFF COMPLIANCE: 0 errors (89+ violations resolved)
- Fixed E999 syntax error in metadata_utils.py
- Refactored 10+ complex functions (C901)
- Updated error handling (S110, S112, SIM105)
- Modernized type hints (UP038)
- Fixed naming conflicts (A001, F811)
BUG FIXES:
- Added missing _on_update_cover() method (11 test failures)
- Fixed BPM display and async loading
- Improved thread safety
TESTING: 30 passed, 1 skipped
DOCUMENTATION: Windows PowerShell support, tox troubleshooting
Diffstat (limited to 'tidal_dl_ng/gui_search.py')
| -rw-r--r-- | tidal_dl_ng/gui_search.py | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/tidal_dl_ng/gui_search.py b/tidal_dl_ng/gui_search.py new file mode 100644 index 0000000..fb303ad --- /dev/null +++ b/tidal_dl_ng/gui_search.py @@ -0,0 +1,220 @@ +# tidal_dl_ng/gui_search.py + +from typing import TYPE_CHECKING, Any + +from tidalapi import Album, Artist, Mix, Playlist, Track, Video +from tidalapi.media import AudioMode +from tidalapi.playlist import Folder +from tidalapi.session import SearchTypes + +from tidal_dl_ng.helper.tidal import ( + get_tidal_media_id, + get_tidal_media_type, + instantiate_media, + name_builder_artist, + quality_audio_highest, + search_results_all, + url_ending_clean, +) +from tidal_dl_ng.logger import logger_gui +from tidal_dl_ng.model.gui_data import ResultItem + +if TYPE_CHECKING: + from tidal_dl_ng.gui import MainWindow + + +class GuiSearchManager: + """Manages the search GUI and logic.""" + + def __init__(self, main_window: "MainWindow"): + """Initialize the search manager.""" + self.main_window: "MainWindow" = main_window + + def search_populate_results(self, query: str, type_media: Any) -> None: + """Populate the results tree with search results.""" + results = self.search(query, [type_media]) + self.main_window.populate_tree_results(results) + + def search(self, query: str, types_media: list[Any]) -> list[ResultItem]: + """Perform a search and return a list of ResultItems.""" + query_clean = query.strip() + if "http" in query_clean: + query_clean = url_ending_clean(query_clean) + media_type = get_tidal_media_type(query_clean) + item_id = get_tidal_media_id(query_clean) + try: + media = instantiate_media(self.main_window.tidal.session, media_type, item_id) + result_search = {"direct": [media]} + except Exception: + logger_gui.error(f"Media not found (ID: {item_id}). Maybe it is not available anymore.") + result_search = {"direct": []} + else: + result_search = search_results_all( + session=self.main_window.tidal.session, needle=query_clean, types_media=types_media + ) + result = [] + for _media_type, l_media in result_search.items(): + if isinstance(l_media, list): + result.extend(self.search_result_to_model(l_media)) + return result + + def search_result_to_model(self, items: list[SearchTypes]) -> list[ResultItem]: + """Convert search results to ResultItem models.""" + return [self._to_result_item(idx, item) for idx, item in enumerate(items) if self._to_result_item(idx, item)] + + def _to_result_item(self, idx: int, item: Any) -> ResultItem | None: + """Helper to convert a single item to ResultItem.""" + if not item or (hasattr(item, "available") and not item.available): + return None + + explicit = " 🅴" if isinstance(item, Track | Video | Album) and item.explicit else "" + date_user_added = ( + item.user_date_added.strftime("%Y-%m-%d_%H:%M") if getattr(item, "user_date_added", None) else "" + ) + date_release = self._get_date_release(item) + + type_handlers = { + Track: self._result_item_from_track, + Video: self._result_item_from_video, + Playlist: self._result_item_from_playlist, + Album: self._result_item_from_album, + Mix: self._result_item_from_mix, + Artist: self._result_item_from_artist, + Folder: self._result_item_from_folder, + } + + for item_type, handler in type_handlers.items(): + if isinstance(item, item_type): + return handler(idx, item, explicit, date_user_added, date_release) + return None + + def _get_date_release(self, item: Any) -> str: + """Get the release date string for an item.""" + if hasattr(item, "album") and item.album and getattr(item.album, "release_date", None): + return item.album.release_date.strftime("%Y-%m-%d_%H:%M") + if hasattr(item, "release_date") and item.release_date: + return item.release_date.strftime("%Y-%m-%d_%H:%M") + return "" + + def _result_item_from_track( + self, idx: int, item: Track, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from a Track.""" + final_quality = quality_audio_highest(item) + if hasattr(item, "audio_modes") and AudioMode.dolby_atmos.value in item.audio_modes: + final_quality = f"{final_quality} / Dolby Atmos" + return ResultItem( + position=idx, + artist=name_builder_artist(item), + title=f"{item.name}{explicit}", + album=item.album.name, + duration_sec=item.duration, + obj=item, + quality=final_quality, + explicit=bool(item.explicit), + date_user_added=date_user_added, + date_release=date_release, + ) + + def _result_item_from_video( + self, idx: int, item: Video, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from a Video.""" + return ResultItem( + position=idx, + artist=name_builder_artist(item), + title=f"{item.name}{explicit}", + album=item.album.name if item.album else "", + duration_sec=item.duration, + obj=item, + quality=item.video_quality, + explicit=bool(item.explicit), + date_user_added=date_user_added, + date_release=date_release, + ) + + def _result_item_from_playlist( + self, idx: int, item: Playlist, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from a Playlist.""" + return ResultItem( + position=idx, + artist=", ".join(artist.name for artist in item.promoted_artists) if item.promoted_artists else "", + title=item.name, + album="", + duration_sec=item.duration, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + date_release=date_release, + ) + + def _result_item_from_album( + self, idx: int, item: Album, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from an Album.""" + return ResultItem( + position=idx, + artist=name_builder_artist(item), + title="", + album=f"{item.name}{explicit}", + duration_sec=item.duration, + obj=item, + quality=quality_audio_highest(item), + explicit=bool(item.explicit), + date_user_added=date_user_added, + date_release=date_release, + ) + + def _result_item_from_mix( + self, idx: int, item: Mix, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from a Mix.""" + return ResultItem( + position=idx, + artist=item.sub_title, + title=item.title, + album="", + duration_sec=-1, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + date_release=date_release, + ) + + def _result_item_from_artist( + self, idx: int, item: Artist, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from an Artist.""" + return ResultItem( + position=idx, + artist=item.name, + title="", + album="", + duration_sec=-1, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + date_release=date_release, + ) + + def _result_item_from_folder( + self, idx: int, item: Folder, explicit: str, date_user_added: str, date_release: str + ) -> ResultItem: + """Create a ResultItem from a Folder.""" + total_items = item.total_number_of_items if hasattr(item, "total_number_of_items") else 0 + return ResultItem( + position=idx, + artist="", + title=f"📁 {item.name} ({total_items} items)", + album="", + duration_sec=-1, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + date_release=date_release, + ) |
