| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- import logging
- from mopidy.backend import LibraryProvider
- from mopidy.models import Ref
- logger = logging.getLogger(__name__)
- class SmartLibrary(LibraryProvider):
- def __init__(self, backend, config):
- super().__init__(backend)
- self.config = config
- self.max_tracks = int(config.get("max_tracks") or 50)
- raw_genres = config.get("genres") or ""
- self.fixed_genres = [g.strip() for g in raw_genres.split(",") if g.strip()]
- self.case_insensitive = bool(config.get("case_insensitive"))
- # Browsing: return Ref objects for directories/tracks
- def browse(self, uri):
- if uri in (None, "smart:", "smart://"):
- # top-level
- return [Ref.directory(uri="smart:genres", name="Genres")]
- if uri == "smart:genres":
- return [
- Ref.directory(uri=f"smart:genre:{g}", name=g.title())
- for g in self.fixed_genres
- ]
- if uri.startswith("smart:genre:"):
- genre = uri.split(":", 2)[2]
- tracks = self._tracks_for_genre(genre)
- return [Ref.track(uri=t.uri, name=t.name) for t in tracks]
- return []
- # Lookup: return full Track models for playback
- def lookup(self, uri):
- if uri.startswith("smart:genre:"):
- genre = uri.split(":", 2)[2]
- return self._tracks_for_genre(genre)
- return []
- # Internal: perform the search via Mopidy core/library API
- def _search_core(self, query):
- """Return list-like of SearchResult objects or [] if unavailable."""
- # Preferred: self.backend.core (available when Mopidy injects core)
- try:
- core = getattr(self.backend, "core", None)
- if core is not None:
- return core.library.search(query)
- except Exception:
- logger.debug("backend.core.library.search failed, falling back to actor RPC", exc_info=True)
- # Fallback: actor RPC (synchronous)
- try:
- actor = getattr(self.backend, "actor", None)
- if actor is not None and hasattr(actor, "root"):
- return actor.root.actor.library.search(query)
- except Exception:
- logger.exception("actor RPC search failed")
- return []
- def _tracks_for_genre(self, genre):
- qval = genre
- if self.case_insensitive:
- # Some backends store lowercase genres; do exact matching here but allow fallback
- qval = genre.lower()
- # Primary query: genre exact
- query = {"genre": [genre]}
- tracks = []
- try:
- results = self._search_core(query)
- for res in results:
- # res may be a SearchResult-like object (has .tracks)
- if hasattr(res, "tracks"):
- for t in res.tracks:
- tracks.append(t)
- if len(tracks) >= self.max_tracks:
- break
- if len(tracks) >= self.max_tracks:
- break
- # If case_insensitive and not enough results, try lowercase variant
- if self.case_insensitive and len(tracks) < self.max_tracks:
- query2 = {"genre": [qval]}
- if query2 != query:
- results2 = self._search_core(query2)
- for res in results2:
- if hasattr(res, "tracks"):
- for t in res.tracks:
- if t not in tracks:
- tracks.append(t)
- if len(tracks) >= self.max_tracks:
- break
- if len(tracks) >= self.max_tracks:
- break
- except Exception:
- logger.exception("Failed to search core for genre %s", genre)
- return tracks[: self.max_tracks]
|