feat: enable rich text links for metadata fields and add search functionality
- Introduced clickable metadata links (tracks, albums, artists, playlists, videos). - Left-click triggers in-app search; Ctrl+click or right-click opens browser search. - Enhanced label interactions and added tooltips for actionable fields. - Simplified artist, album, and playlist rendering with rich links. - Extended search functionality with URL-safe encoding and type mapping.
This commit is contained in:
@@ -48,6 +48,7 @@ import contextlib
|
|||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
from collections.abc import Callable, Iterable, Sequence
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -863,6 +864,52 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
# Managers
|
# Managers
|
||||||
self.queue_manager.connect_signals()
|
self.queue_manager.connect_signals()
|
||||||
self.playlist_manager.connect_signals()
|
self.playlist_manager.connect_signals()
|
||||||
|
self.info_tab_widget.s_search_in_app.connect(self.on_search_in_app)
|
||||||
|
self.info_tab_widget.s_search_in_browser.connect(self.on_search_in_browser)
|
||||||
|
|
||||||
|
def on_search_in_app(self, search_term: str, search_type: str):
|
||||||
|
"""Perform a search within the application, selecting the correct category."""
|
||||||
|
self.l_search.setText(search_term)
|
||||||
|
|
||||||
|
# Map the search type string to the corresponding class
|
||||||
|
search_type_map = {
|
||||||
|
"artist": Artist,
|
||||||
|
"album": Album,
|
||||||
|
"track": Track,
|
||||||
|
"video": Video,
|
||||||
|
"playlist": Playlist,
|
||||||
|
}
|
||||||
|
# Default to the current selection if the type is unknown
|
||||||
|
search_category = search_type_map.get(search_type.lower(), self.cb_search_type.currentData())
|
||||||
|
|
||||||
|
# Find the index of the category in the combobox and set it
|
||||||
|
for i in range(self.cb_search_type.count()):
|
||||||
|
if self.cb_search_type.itemData(i) == search_category:
|
||||||
|
self.cb_search_type.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
self.search_manager.search_populate_results(search_term, search_category)
|
||||||
|
|
||||||
|
def on_search_in_browser(self, search_term: str, search_type: str):
|
||||||
|
"""Open a search in the default web browser."""
|
||||||
|
safe_term = urllib.parse.quote(search_term)
|
||||||
|
# Map the search type to the URL path component
|
||||||
|
search_path_map = {
|
||||||
|
"artist": "artists",
|
||||||
|
"album": "albums",
|
||||||
|
"track": "tracks",
|
||||||
|
"video": "videos",
|
||||||
|
"playlist": "playlists",
|
||||||
|
}
|
||||||
|
# Use a generic search if the type is not in the map
|
||||||
|
search_path = search_path_map.get(search_type.lower())
|
||||||
|
|
||||||
|
if search_path:
|
||||||
|
url = QtCore.QUrl(f"https://listen.tidal.com/search/{search_path}?q={safe_term}")
|
||||||
|
else:
|
||||||
|
url = QtCore.QUrl(f"https://listen.tidal.com/search?q={safe_term}")
|
||||||
|
|
||||||
|
QtGui.QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def on_logout(self) -> None:
|
def on_logout(self) -> None:
|
||||||
"""Log out from TIDAL and close the application."""
|
"""Log out from TIDAL and close the application."""
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ class TrackInfoFormatter:
|
|||||||
artists = getattr(media, "artists", [])
|
artists = getattr(media, "artists", [])
|
||||||
|
|
||||||
for artist in artists:
|
for artist in artists:
|
||||||
name = InfoTabWidget._get_artist_name(artist)
|
name = TrackInfoFormatter._get_artist_name(artist)
|
||||||
roles = InfoTabWidget._format_artist_roles(artist)
|
roles = TrackInfoFormatter._format_artist_roles(artist)
|
||||||
|
|
||||||
if roles and roles[0] != "main":
|
if roles and roles[0] != "main":
|
||||||
name = f"{name} ({', '.join(roles)})"
|
name = f"{name} ({', '.join(roles)})"
|
||||||
@@ -207,6 +207,8 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
s_update_cover: QtCore.Signal = QtCore.Signal(str)
|
s_update_cover: QtCore.Signal = QtCore.Signal(str)
|
||||||
s_spinner_start: QtCore.Signal = QtCore.Signal()
|
s_spinner_start: QtCore.Signal = QtCore.Signal()
|
||||||
s_spinner_stop: QtCore.Signal = QtCore.Signal()
|
s_spinner_stop: QtCore.Signal = QtCore.Signal()
|
||||||
|
s_search_in_app: QtCore.Signal = QtCore.Signal(str, str)
|
||||||
|
s_search_in_browser: QtCore.Signal = QtCore.Signal(str, str)
|
||||||
|
|
||||||
def _setup_search_roots(self, parent_widget: QtWidgets.QTabWidget | None) -> list:
|
def _setup_search_roots(self, parent_widget: QtWidgets.QTabWidget | None) -> list:
|
||||||
"""Setup list of widget roots to search for UI elements."""
|
"""Setup list of widget roots to search for UI elements."""
|
||||||
@@ -307,6 +309,60 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
# connect signals
|
# connect signals
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
|
|
||||||
|
# Make labels clickable using linkActivated signal
|
||||||
|
clickable_labels = [self.lbl_title, self.lbl_artists, self.lbl_album]
|
||||||
|
for label in clickable_labels:
|
||||||
|
label.setTextFormat(QtCore.Qt.RichText)
|
||||||
|
label.setOpenExternalLinks(False) # We handle links manually
|
||||||
|
label.linkActivated.connect(self._on_link_activated)
|
||||||
|
label.installEventFilter(self) # For right-click handling
|
||||||
|
label.setToolTip("Left-click to search in app, Ctrl+click or right-click to search in browser.")
|
||||||
|
|
||||||
|
def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool:
|
||||||
|
"""Filter events for clickable labels to handle right-clicks."""
|
||||||
|
if event.type() == QtCore.QEvent.Type.MouseButtonPress and isinstance(watched, QtWidgets.QLabel):
|
||||||
|
event: QtGui.QMouseEvent
|
||||||
|
if event.button() == QtCore.Qt.MouseButton.RightButton:
|
||||||
|
# On right-click, find the link under the cursor
|
||||||
|
anchor = watched.anchorAt(event.pos())
|
||||||
|
if anchor:
|
||||||
|
self._on_link_activated(anchor, right_click=True)
|
||||||
|
return True # Event handled
|
||||||
|
|
||||||
|
return super().eventFilter(watched, event)
|
||||||
|
|
||||||
|
def _create_link(self, search_type: str, text: str) -> str:
|
||||||
|
"""Create an HTML link for search, using a specific color."""
|
||||||
|
if not text or text == "—":
|
||||||
|
return "—"
|
||||||
|
# The link format is "search:type:text"
|
||||||
|
link_target = f"search:{search_type}:{text}"
|
||||||
|
# Use a standard link color; Qt will handle styling.
|
||||||
|
return f"<a href='{link_target}' style='color:#367af6;'>{text}</a>"
|
||||||
|
|
||||||
|
@QtCore.Slot(str)
|
||||||
|
def _on_link_activated(self, link: str, right_click: bool = False) -> None:
|
||||||
|
"""Handle clicks on rich text links in labels."""
|
||||||
|
if not link:
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = link.split(":", 2)
|
||||||
|
if len(parts) < 3 or parts[0] != "search":
|
||||||
|
return
|
||||||
|
|
||||||
|
search_type = parts[1]
|
||||||
|
search_term = parts[2]
|
||||||
|
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||||
|
|
||||||
|
is_browser_action = right_click or (modifiers & QtCore.Qt.KeyboardModifier.ControlModifier)
|
||||||
|
|
||||||
|
if is_browser_action:
|
||||||
|
# Ctrl+Click or Right-Click: Open in browser
|
||||||
|
self.s_search_in_browser.emit(search_term, search_type)
|
||||||
|
else:
|
||||||
|
# Left-Click: Search in app
|
||||||
|
self.s_search_in_app.emit(search_term, search_type)
|
||||||
|
|
||||||
def _connect_signals(self) -> None:
|
def _connect_signals(self) -> None:
|
||||||
"""Connect internal signals to slots."""
|
"""Connect internal signals to slots."""
|
||||||
self.s_update_details.connect(self._on_update_details)
|
self.s_update_details.connect(self._on_update_details)
|
||||||
@@ -520,21 +576,28 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
"""
|
"""
|
||||||
# Title and version
|
# Title and version
|
||||||
title = name_builder_title(track)
|
title = name_builder_title(track)
|
||||||
|
self.lbl_title.setText(self._create_link("track", title))
|
||||||
version = getattr(track, "version", None)
|
version = getattr(track, "version", None)
|
||||||
|
|
||||||
# Use _safe_str for all displayed values to avoid passing non-str types to QLabel.setText
|
|
||||||
self.lbl_title.setText(safe_str(title))
|
|
||||||
self.lbl_version.setText(safe_str(version))
|
self.lbl_version.setText(safe_str(version))
|
||||||
|
|
||||||
# Artists
|
# Artists
|
||||||
artists = TrackInfoFormatter.format_artists(track)
|
artist_parts = []
|
||||||
self.lbl_artists.setText(safe_str(artists))
|
if hasattr(track, "artists") and track.artists:
|
||||||
|
for artist in track.artists:
|
||||||
|
name = TrackInfoFormatter._get_artist_name(artist)
|
||||||
|
roles = TrackInfoFormatter._format_artist_roles(artist)
|
||||||
|
link = self._create_link("artist", name)
|
||||||
|
if roles and roles[0] != "main":
|
||||||
|
artist_parts.append(f"{link} ({', '.join(roles)})")
|
||||||
|
else:
|
||||||
|
artist_parts.append(link)
|
||||||
|
self.lbl_artists.setText(", ".join(artist_parts) if artist_parts else "—")
|
||||||
|
|
||||||
# Album name (prefer album object's name)
|
# Album name (prefer album object's name)
|
||||||
album_name = None
|
album_name = None
|
||||||
if hasattr(track, "album") and track.album:
|
if hasattr(track, "album") and track.album:
|
||||||
album_name = getattr(track.album, "name", None) or None
|
album_name = getattr(track.album, "name", None) or None
|
||||||
self.lbl_album.setText(safe_str(album_name))
|
self.lbl_album.setText(self._create_link("album", album_name))
|
||||||
|
|
||||||
# Codec & Bitrate
|
# Codec & Bitrate
|
||||||
codec = TrackInfoFormatter.format_codec(track)
|
codec = TrackInfoFormatter.format_codec(track)
|
||||||
@@ -557,22 +620,18 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
self.lbl_popularity.setText(safe_str(popularity))
|
self.lbl_popularity.setText(safe_str(popularity))
|
||||||
|
|
||||||
# BPM: Show loading indicator, will be updated when extras are loaded
|
# BPM: Show loading indicator, will be updated when extras are loaded
|
||||||
# The BPM comes from async extras, not from the track object itself
|
|
||||||
self.lbl_bpm.setText("⏳ Loading...")
|
self.lbl_bpm.setText("⏳ Loading...")
|
||||||
|
|
||||||
# ISRC
|
# ISRC
|
||||||
isrc = getattr(track, "isrc", None)
|
isrc = getattr(track, "isrc", None)
|
||||||
self.lbl_isrc.setText(safe_str(isrc))
|
self.lbl_isrc.setText(safe_str(isrc))
|
||||||
|
|
||||||
# Track Number: try multiple possible attribute names and album track info
|
# Track Number
|
||||||
track_number = find_attr(track, "track_number", "tracknumber", "number", "position", "track") or None
|
track_number = find_attr(track, "track_number", "tracknumber", "number", "position", "track") or None
|
||||||
if track_number is None and hasattr(track, "album") and track.album:
|
if track_number is None and hasattr(track, "album") and track.album:
|
||||||
# Some album objects contain track indexing information
|
|
||||||
track_number = find_attr(track.album, "track_number", "tracknumber", "number") or None
|
track_number = find_attr(track.album, "track_number", "tracknumber", "number") or None
|
||||||
self.lbl_track_number.setText(safe_str(track_number))
|
self.lbl_track_number.setText(safe_str(track_number))
|
||||||
|
|
||||||
# Note: Label, Genres, Producers and Composers fields are not available in TIDAL API - removed from display
|
|
||||||
|
|
||||||
def _populate_video_details(self, video: Video) -> None:
|
def _populate_video_details(self, video: Video) -> None:
|
||||||
"""Populate details for a Video.
|
"""Populate details for a Video.
|
||||||
|
|
||||||
@@ -581,27 +640,31 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
"""
|
"""
|
||||||
# Title and version
|
# Title and version
|
||||||
title = name_builder_title(video)
|
title = name_builder_title(video)
|
||||||
|
self.lbl_title.setText(self._create_link("video", title))
|
||||||
version = getattr(video, "version", None)
|
version = getattr(video, "version", None)
|
||||||
|
|
||||||
self.lbl_title.setText(safe_str(title))
|
|
||||||
self.lbl_version.setText(safe_str(version))
|
self.lbl_version.setText(safe_str(version))
|
||||||
|
|
||||||
# Artists (from credits if available)
|
# Artists
|
||||||
artists = extract_names_from_mixed(
|
artists_str = extract_names_from_mixed(
|
||||||
getattr(video, "credits", None), match_types=("performer", "artist", "actor")
|
getattr(video, "credits", None), match_types=("performer", "artist", "actor")
|
||||||
)
|
)
|
||||||
if not artists:
|
if not artists_str:
|
||||||
# Fallback to regular artist field
|
artist_parts = []
|
||||||
artists = TrackInfoFormatter.format_artists(video)
|
if hasattr(video, "artists") and video.artists:
|
||||||
self.lbl_artists.setText(safe_str(artists))
|
for artist in video.artists:
|
||||||
|
name = TrackInfoFormatter._get_artist_name(artist)
|
||||||
|
artist_parts.append(self._create_link("artist", name))
|
||||||
|
self.lbl_artists.setText(", ".join(artist_parts) if artist_parts else "—")
|
||||||
|
else:
|
||||||
|
self.lbl_artists.setText(self._create_link("artist", artists_str))
|
||||||
|
|
||||||
# Album name (from album object if available)
|
# Album name
|
||||||
album_name = None
|
album_name = None
|
||||||
if hasattr(video, "album") and video.album:
|
if hasattr(video, "album") and video.album:
|
||||||
album_name = getattr(video.album, "name", None) or None
|
album_name = getattr(video.album, "name", None) or None
|
||||||
self.lbl_album.setText(safe_str(album_name))
|
self.lbl_album.setText(self._create_link("album", album_name))
|
||||||
|
|
||||||
# Codec & Bitrate (inherited from Track)
|
# Codec & Bitrate
|
||||||
codec = TrackInfoFormatter.format_codec(video)
|
codec = TrackInfoFormatter.format_codec(video)
|
||||||
self.lbl_codec.setText(safe_str(codec))
|
self.lbl_codec.setText(safe_str(codec))
|
||||||
bitrate = TrackInfoFormatter.format_bitrate(video)
|
bitrate = TrackInfoFormatter.format_bitrate(video)
|
||||||
@@ -611,7 +674,7 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
duration = TrackInfoFormatter.format_duration(getattr(video, "duration", None))
|
duration = TrackInfoFormatter.format_duration(getattr(video, "duration", None))
|
||||||
self.lbl_duration.setText(safe_str(duration))
|
self.lbl_duration.setText(safe_str(duration))
|
||||||
|
|
||||||
# Release date (from album if exists)
|
# Release date
|
||||||
release_date = None
|
release_date = None
|
||||||
if hasattr(video, "album") and video.album and hasattr(video.album, "release_date"):
|
if hasattr(video, "album") and video.album and hasattr(video.album, "release_date"):
|
||||||
release_date = TrackInfoFormatter.format_date(video.album.release_date)
|
release_date = TrackInfoFormatter.format_date(video.album.release_date)
|
||||||
@@ -621,15 +684,12 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
popularity = getattr(video, "popularity", None)
|
popularity = getattr(video, "popularity", None)
|
||||||
self.lbl_popularity.setText(safe_str(popularity))
|
self.lbl_popularity.setText(safe_str(popularity))
|
||||||
|
|
||||||
# ISRC (from audio track if available)
|
# ISRC
|
||||||
isrc = getattr(video, "isrc", None)
|
isrc = getattr(video, "isrc", None)
|
||||||
if not isrc and hasattr(video, "track"):
|
if not isrc and hasattr(video, "track"):
|
||||||
isrc = getattr(video.track, "isrc", None)
|
isrc = getattr(video.track, "isrc", None)
|
||||||
self.lbl_isrc.setText(safe_str(isrc))
|
self.lbl_isrc.setText(safe_str(isrc))
|
||||||
|
|
||||||
# Note: Video-specific fields (resolution, frame rate) and genres are not displayed
|
|
||||||
# as the required labels are not available in the UI
|
|
||||||
|
|
||||||
def _populate_album_details(self, album: Album) -> None:
|
def _populate_album_details(self, album: Album) -> None:
|
||||||
"""Populate details for an Album.
|
"""Populate details for an Album.
|
||||||
|
|
||||||
@@ -638,26 +698,28 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
"""
|
"""
|
||||||
# Title and version
|
# Title and version
|
||||||
title = name_builder_title(album)
|
title = name_builder_title(album)
|
||||||
|
self.lbl_title.setText(self._create_link("album", title))
|
||||||
version = getattr(album, "version", None)
|
version = getattr(album, "version", None)
|
||||||
|
|
||||||
self.lbl_title.setText(safe_str(title))
|
|
||||||
self.lbl_version.setText(safe_str(version))
|
self.lbl_version.setText(safe_str(version))
|
||||||
|
|
||||||
# Artists (from album object)
|
# Artists
|
||||||
artists = TrackInfoFormatter.format_artists(album)
|
artist_parts = []
|
||||||
self.lbl_artists.setText(safe_str(artists))
|
if hasattr(album, "artists") and album.artists:
|
||||||
|
for artist in album.artists:
|
||||||
|
name = TrackInfoFormatter._get_artist_name(artist)
|
||||||
|
artist_parts.append(self._create_link("artist", name))
|
||||||
|
self.lbl_artists.setText(", ".join(artist_parts) if artist_parts else "—")
|
||||||
|
|
||||||
# Album name
|
# Album name (same as title for an album)
|
||||||
album_name = getattr(album, "name", None)
|
self.lbl_album.setText(self._create_link("album", title))
|
||||||
self.lbl_album.setText(safe_str(album_name))
|
|
||||||
|
|
||||||
# Codec & Bitrate: not typically relevant for albums, but can inherit from first track
|
# Codec & Bitrate
|
||||||
codec = TrackInfoFormatter.format_codec(album)
|
codec = TrackInfoFormatter.format_codec(album)
|
||||||
self.lbl_codec.setText(safe_str(codec))
|
self.lbl_codec.setText(safe_str(codec))
|
||||||
bitrate = TrackInfoFormatter.format_bitrate(album)
|
bitrate = TrackInfoFormatter.format_bitrate(album)
|
||||||
self.lbl_bitrate.setText(safe_str(bitrate))
|
self.lbl_bitrate.setText(safe_str(bitrate))
|
||||||
|
|
||||||
# Duration: total duration of the album
|
# Duration
|
||||||
duration = TrackInfoFormatter.format_duration(getattr(album, "duration", None))
|
duration = TrackInfoFormatter.format_duration(getattr(album, "duration", None))
|
||||||
self.lbl_duration.setText(safe_str(duration))
|
self.lbl_duration.setText(safe_str(duration))
|
||||||
|
|
||||||
@@ -665,15 +727,12 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
release_date = TrackInfoFormatter.format_date(getattr(album, "release_date", None))
|
release_date = TrackInfoFormatter.format_date(getattr(album, "release_date", None))
|
||||||
self.lbl_release_date.setText(safe_str(release_date))
|
self.lbl_release_date.setText(safe_str(release_date))
|
||||||
|
|
||||||
# Popularity: album popularity if available
|
# Popularity
|
||||||
popularity = getattr(album, "popularity", None)
|
popularity = getattr(album, "popularity", None)
|
||||||
self.lbl_popularity.setText(safe_str(popularity))
|
self.lbl_popularity.setText(safe_str(popularity))
|
||||||
|
|
||||||
# ISRC: not typically available at album level, clear field
|
|
||||||
self.lbl_isrc.setText("—")
|
self.lbl_isrc.setText("—")
|
||||||
|
|
||||||
# Note: Genres, Label, Producers and Composers fields are not available in TIDAL API - removed from display
|
|
||||||
|
|
||||||
def _extract_playlist_artists(self, playlist: Playlist | Mix) -> str:
|
def _extract_playlist_artists(self, playlist: Playlist | Mix) -> str:
|
||||||
"""Extract and format artists from playlist tracks."""
|
"""Extract and format artists from playlist tracks."""
|
||||||
if not hasattr(playlist, "tracks") or not isinstance(playlist.tracks, list | Iterable):
|
if not hasattr(playlist, "tracks") or not isinstance(playlist.tracks, list | Iterable):
|
||||||
@@ -681,13 +740,15 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
|
|
||||||
all_artists = set()
|
all_artists = set()
|
||||||
for track in playlist.tracks:
|
for track in playlist.tracks:
|
||||||
if isinstance(track, Track | Video | Album):
|
if isinstance(track, Track | Video | Album) and hasattr(track, "artists"):
|
||||||
artists = TrackInfoFormatter.format_artists(track)
|
for artist in track.artists:
|
||||||
if artists and artists != "Unknown Artist":
|
name = TrackInfoFormatter._get_artist_name(artist)
|
||||||
all_artists.update(artists.split(", "))
|
if name and name != "Unknown Artist":
|
||||||
|
all_artists.add(name)
|
||||||
|
|
||||||
if all_artists:
|
if all_artists:
|
||||||
return ", ".join(list(all_artists)[:3])
|
artist_links = [self._create_link("artist", name) for name in list(all_artists)[:5]]
|
||||||
|
return ", ".join(artist_links)
|
||||||
return "—"
|
return "—"
|
||||||
|
|
||||||
def _extract_playlist_release_date(self, playlist: Playlist | Mix) -> str:
|
def _extract_playlist_release_date(self, playlist: Playlist | Mix) -> str:
|
||||||
@@ -712,26 +773,23 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
"""
|
"""
|
||||||
# Title and version
|
# Title and version
|
||||||
title = name_builder_title(playlist)
|
title = name_builder_title(playlist)
|
||||||
|
self.lbl_title.setText(self._create_link("playlist", title))
|
||||||
version = getattr(playlist, "version", None)
|
version = getattr(playlist, "version", None)
|
||||||
|
|
||||||
self.lbl_title.setText(safe_str(title))
|
|
||||||
self.lbl_version.setText(safe_str(version))
|
self.lbl_version.setText(safe_str(version))
|
||||||
|
|
||||||
# Artists: try to combine artists from all tracks
|
# Artists
|
||||||
artists = self._extract_playlist_artists(playlist)
|
artists_html = self._extract_playlist_artists(playlist)
|
||||||
self.lbl_artists.setText(safe_str(artists))
|
self.lbl_artists.setText(artists_html)
|
||||||
|
|
||||||
# Album name: prefer playlist name, then mix/album name
|
# Album name (playlist title)
|
||||||
album_name = getattr(playlist, "name", None)
|
album_name = getattr(playlist, "name", None)
|
||||||
if not album_name and hasattr(playlist, "album"):
|
self.lbl_album.setText(self._create_link("playlist", album_name))
|
||||||
album_name = getattr(playlist.album, "name", None)
|
|
||||||
self.lbl_album.setText(safe_str(album_name))
|
|
||||||
|
|
||||||
# Codec & Bitrate: not typically relevant for playlists, clear fields
|
# Codec & Bitrate
|
||||||
self.lbl_codec.setText("—")
|
self.lbl_codec.setText("—")
|
||||||
self.lbl_bitrate.setText("—")
|
self.lbl_bitrate.setText("—")
|
||||||
|
|
||||||
# Duration: total duration of the playlist/mix
|
# Duration
|
||||||
duration = TrackInfoFormatter.format_duration(getattr(playlist, "duration", None))
|
duration = TrackInfoFormatter.format_duration(getattr(playlist, "duration", None))
|
||||||
self.lbl_duration.setText(safe_str(duration))
|
self.lbl_duration.setText(safe_str(duration))
|
||||||
|
|
||||||
@@ -739,15 +797,12 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
release_date = self._extract_playlist_release_date(playlist)
|
release_date = self._extract_playlist_release_date(playlist)
|
||||||
self.lbl_release_date.setText(safe_str(release_date))
|
self.lbl_release_date.setText(safe_str(release_date))
|
||||||
|
|
||||||
# Popularity: playlist popularity if available
|
# Popularity
|
||||||
popularity = getattr(playlist, "popularity", None)
|
popularity = getattr(playlist, "popularity", None)
|
||||||
self.lbl_popularity.setText(safe_str(popularity))
|
self.lbl_popularity.setText(safe_str(popularity))
|
||||||
|
|
||||||
# ISRC: not typically available at playlist level, clear field
|
|
||||||
self.lbl_isrc.setText("—")
|
self.lbl_isrc.setText("—")
|
||||||
|
|
||||||
# Note: Genres, Label, Producers and Composers fields are not available in TIDAL API - removed from display
|
|
||||||
|
|
||||||
def _extract_artist_album_name(self, artist: Artist) -> str | None:
|
def _extract_artist_album_name(self, artist: Artist) -> str | None:
|
||||||
"""Extract first album name from artist's albums."""
|
"""Extract first album name from artist's albums."""
|
||||||
if not hasattr(artist, "albums") or not isinstance(artist.albums, list | Iterable):
|
if not hasattr(artist, "albums") or not isinstance(artist.albums, list | Iterable):
|
||||||
@@ -789,13 +844,11 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
Args:
|
Args:
|
||||||
artist (Artist): The artist object.
|
artist (Artist): The artist object.
|
||||||
"""
|
"""
|
||||||
# Name: prefer artist's own name, fallback to common name
|
# Name
|
||||||
name = getattr(artist, "name", None)
|
name = getattr(artist, "name", None)
|
||||||
if not name and hasattr(artist, "common_name"):
|
self.lbl_title.setText(self._create_link("artist", name))
|
||||||
name = getattr(artist.common_name, "name", None)
|
|
||||||
self.lbl_title.setText(safe_str(name))
|
|
||||||
|
|
||||||
# Version: use artist type or role if available
|
# Version
|
||||||
version = None
|
version = None
|
||||||
if hasattr(artist, "type"):
|
if hasattr(artist, "type"):
|
||||||
version = getattr(artist.type, "name", None)
|
version = getattr(artist.type, "name", None)
|
||||||
@@ -803,31 +856,28 @@ class InfoTabWidget(QtCore.QObject):
|
|||||||
version = getattr(artist.role, "name", None)
|
version = getattr(artist.role, "name", None)
|
||||||
self.lbl_version.setText(safe_str(version))
|
self.lbl_version.setText(safe_str(version))
|
||||||
|
|
||||||
# Artists: typically not relevant for single artist, clear field
|
# Artists
|
||||||
self.lbl_artists.setText("—")
|
self.lbl_artists.setText("—")
|
||||||
|
|
||||||
# Album name: try to use first album name if available
|
# Album name
|
||||||
album_name = self._extract_artist_album_name(artist)
|
album_name = self._extract_artist_album_name(artist)
|
||||||
self.lbl_album.setText(safe_str(album_name))
|
self.lbl_album.setText(self._create_link("album", album_name))
|
||||||
|
|
||||||
# Codec & Bitrate: not relevant for artists, clear fields
|
# Codec & Bitrate
|
||||||
self.lbl_codec.setText("—")
|
self.lbl_codec.setText("—")
|
||||||
self.lbl_bitrate.setText("—")
|
self.lbl_bitrate.setText("—")
|
||||||
|
|
||||||
# Duration: total duration of all albums/tracks by the artist
|
# Duration
|
||||||
total_duration = self._calculate_artist_total_duration(artist)
|
total_duration = self._calculate_artist_total_duration(artist)
|
||||||
duration = TrackInfoFormatter.format_duration(total_duration)
|
duration = TrackInfoFormatter.format_duration(total_duration)
|
||||||
self.lbl_duration.setText(safe_str(duration))
|
self.lbl_duration.setText(safe_str(duration))
|
||||||
|
|
||||||
# Release date: try to use release date of the first album
|
# Release date
|
||||||
release_date = self._extract_artist_release_date(artist)
|
release_date = self._extract_artist_release_date(artist)
|
||||||
self.lbl_release_date.setText(safe_str(release_date))
|
self.lbl_release_date.setText(safe_str(release_date))
|
||||||
|
|
||||||
# Popularity: artist popularity if available
|
# Popularity
|
||||||
popularity = getattr(artist, "popularity", None)
|
popularity = getattr(artist, "popularity", None)
|
||||||
self.lbl_popularity.setText(safe_str(popularity))
|
self.lbl_popularity.setText(safe_str(popularity))
|
||||||
|
|
||||||
# ISRC: not typically available at artist level, clear field
|
|
||||||
self.lbl_isrc.setText("—")
|
self.lbl_isrc.setText("—")
|
||||||
|
|
||||||
# Note: Genres, Label, Producers and Composers fields are not available in TIDAL API - removed from display
|
|
||||||
|
|||||||
Reference in New Issue
Block a user