Colin Powell 3 týždňov pred
rodič
commit
aa1c29d1d6

+ 1 - 26
mopidy_smartplaylists/__init__.py

@@ -1,26 +1 @@
-import pathlib
-
-from mopidy import config, ext
-from importlib.metadata import version, PackageNotFoundError
-
-class Extension(ext.Extension):
-
-    dist_name = "Mopidy-SmartPlaylists"
-    ext_name = "smartplaylists"
-    version = "0.1.0"
-
-    def get_default_config(self):
-        return config.read(pathlib.Path(__file__).parent / "ext.conf")
-
-    def get_config_schema(self):
-        schema = super().get_config_schema()
-        schema["genres"] = config.String()
-        schema["max_tracks"] = config.Integer()
-        return schema
-
-    def setup(self, registry):
-        from .backend import SmartPlaylistsBackend
-
-        registry.add("backend", SmartPlaylistsBackend)
-
-
+from .extension import Extension

+ 5 - 0
mopidy_smartplaylists/backend.py

@@ -1,10 +1,15 @@
+import logging
 import mopidy.backend
 from .library import SmartLibrary
 
+logger = logging.getLogger(__name__)
+
 class SmartPlaylistsBackend(mopidy.backend.Backend):
+    """Backend that exposes a LibraryProvider for smart playlists."""
     uri_schemes = ["smart"]
 
     def __init__(self, config, audio):
         super().__init__(audio=audio)
         self.config = config["smartplaylists"]
+        # Provide library provider that implements browse/lookup
         self.library = SmartLibrary(backend=self, config=self.config)

+ 12 - 23
mopidy_smartplaylists/extension.py

@@ -1,32 +1,21 @@
+import pathlib
 from mopidy import ext, config
 
-
 class Extension(ext.Extension):
     dist_name = "mopidy-smartplaylists"
     ext_name = "smartplaylists"
     version = "0.3.0"
 
+    def get_default_config(self):
+        return config.read(pathlib.Path(__file__).parent / "ext.conf")
 
-def get_default_config(self):
-    return config.read("""
-        [smartplaylists]
-        # Comma-separated popular genres to expose as top-level folders
-        genres = rock,jazz,blues,pop,electronic,hiphop
-        # Maximum number of tracks to return per genre
-        max_tracks = 50
-        # Use case-insensitive matching for genres
-        case_insensitive = true
-    """)
-
-
-def get_config_schema(self):
-    schema = super().get_config_schema()
-    schema["genres"] = config.String(optional=True)
-    schema["max_tracks"] = config.Integer(optional=True)
-    schema["case_insensitive"] = config.Boolean(optional=True)
-    return schema
-
+    def get_config_schema(self):
+        schema = super().get_config_schema()
+        schema["genres"] = config.String(optional=True)
+        schema["max_tracks"] = config.Integer(optional=True)
+        schema["case_insensitive"] = config.Boolean(optional=True)
+        return schema
 
-def setup(self, registry):
-    from .backend import SmartPlaylistsBackend
-    registry.add("backend", SmartPlaylistsBackend)
+    def setup(self, registry):
+        from .backend import SmartPlaylistsBackend
+        registry.add("backend", SmartPlaylistsBackend)

+ 72 - 19
mopidy_smartplaylists/library.py

@@ -1,49 +1,102 @@
+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 == "smart:":
-            return [
-                Ref.directory(uri="smart:genres", name="Genres"),
-                Ref.directory(uri="smart:artists", name="Artists"),
-            ]
+        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 ["rock", "jazz", "blues"]
+                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 [Ref.track(uri=t.uri, name=t.name) for t in tracks]
 
         return []
 
+    # Lookup: return full Track models for playback
     def lookup(self, uri):
-        # return full Track models for playback
         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):
-        # WORKS: Use Mopidy’s global search API
-        results = self.backend.actor.root.actor.library.search(
-            {"genre": [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 = []
-        for r in results:
-            if r.tracks:
-                tracks.extend(r.tracks)
-        return tracks[:50]   # enforce max
+        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]