You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/core/search/library_check.py

124 lines
4.5 KiB

"""Batch library presence check for search results.
Given a list of `albums` and `tracks` from a metadata search, return per-row
booleans (and matched-row metadata for tracks) indicating whether each
result is already in the user's library or wishlist. Plex relative-path
thumb URLs are rewritten to absolute URLs with token.
Called async from the frontend after the main search renders, so the user
sees results immediately and "in library" badges fade in once the check
completes.
"""
from __future__ import annotations
import logging
from typing import Optional
from core.wishlist.presence import load_wishlist_keys as _load_wishlist_keys_shared
logger = logging.getLogger(__name__)
def _resolve_plex_thumb(thumb: str, plex_base: str, plex_token: str) -> str:
"""Rewrite a Plex relative thumb path to an absolute URL with token."""
if not thumb or thumb.startswith('http') or not plex_base or not thumb.startswith('/'):
return thumb
if plex_token:
return f"{plex_base}{thumb}?X-Plex-Token={plex_token}"
return f"{plex_base}{thumb}"
def _resolve_plex_credentials(plex_client, config_manager) -> tuple[str, str]:
"""Pull (base_url, token) for the active Plex server.
Prefers the live `plex_client.server` attrs; falls back to config_manager
if the live client isn't connected yet. Mirrors original web_server.py
inline logic byte-for-byte.
"""
base, token = '', ''
if plex_client and plex_client.server:
base = getattr(plex_client.server, '_baseurl', '') or ''
token = getattr(plex_client.server, '_token', '') or ''
if not base:
cfg = config_manager.get_plex_config()
base = (cfg.get('base_url', '') or '').rstrip('/')
token = token or cfg.get('token', '')
return base, token
def _load_wishlist_keys(cursor, profile_id: int) -> set[str]:
return _load_wishlist_keys_shared(cursor, profile_id)
def check_library_presence(
database,
plex_client,
config_manager,
profile_id: int,
albums: list[dict],
tracks: list[dict],
) -> dict:
"""Return `{albums: [bool], tracks: [{...}]}` for the given search results.
- `albums` returns one bool per input row.
- `tracks` returns one dict per input row. Matched rows get the full
track metadata + resolved thumb URL; unmatched rows get
`{in_library: False, in_wishlist: bool}`.
"""
conn = database._get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"SELECT LOWER(al.title) || '|||' || LOWER(ar.name) "
"FROM albums al JOIN artists ar ON ar.id = al.artist_id"
)
owned_albums = {r[0] for r in cursor.fetchall()}
cursor.execute(
"""
SELECT LOWER(t.title) || '|||' || LOWER(a.name), t.id, t.file_path,
t.title, a.name, al.title, al.thumb_url
FROM tracks t
JOIN artists a ON a.id = t.artist_id
JOIN albums al ON al.id = t.album_id
"""
)
owned_tracks: dict[str, dict] = {}
for r in cursor.fetchall():
if r[0] not in owned_tracks: # keep first match only
owned_tracks[r[0]] = {
'track_id': r[1],
'file_path': r[2],
'title': r[3],
'artist_name': r[4],
'album_title': r[5],
'album_thumb_url': r[6],
}
wishlist_keys = _load_wishlist_keys(cursor, profile_id)
album_results: list[bool] = []
for a in albums:
key = (a.get('name', '').lower() + '|||' + a.get('artist', '').split(',')[0].strip().lower())
album_results.append(key in owned_albums)
plex_base, plex_token = _resolve_plex_credentials(plex_client, config_manager)
track_results: list[dict] = []
for t in tracks:
key = (t.get('name', '').lower() + '|||' + t.get('artist', '').split(',')[0].strip().lower())
in_wishlist = key in wishlist_keys
match = owned_tracks.get(key)
if match:
thumb = match.get('album_thumb_url') or ''
match['album_thumb_url'] = _resolve_plex_thumb(thumb, plex_base, plex_token)
track_results.append({'in_library': True, 'in_wishlist': in_wishlist, **match})
else:
track_results.append({'in_library': False, 'in_wishlist': in_wishlist})
finally:
conn.close()
return {'albums': album_results, 'tracks': track_results}