File: library.py

package info (click to toggle)
mopidy-beets 4.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 224 kB
  • sloc: python: 627; makefile: 3
file content (193 lines) | stat: -rw-r--r-- 7,842 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import logging
import re

from mopidy import backend, models
from mopidy.models import SearchResult

from mopidy_beets.browsers.albums import (
    AlbumsByArtistBrowser,
    AlbumsByGenreBrowser,
    AlbumsByYearBrowser,
)
from mopidy_beets.translator import assemble_uri, parse_uri


logger = logging.getLogger(__name__)

# match dates of the following format:
#   YYYY, YYYY-MM, YYYY-MM-DD, YYYY/MM, YYYY/MM/DD
DATE_REGEX = re.compile(
    r"^(?P<year>\d{4})(?:[-/](?P<month>\d{1,2})(?:[-/](?P<day>\d{1,2}))?)?$"
)


class BeetsLibraryProvider(backend.LibraryProvider):

    root_directory = models.Ref.directory(
        uri="beets:library", name="Beets library"
    )
    root_categorie_list = [
        ("albums-by-artist", "Albums by Artist", AlbumsByArtistBrowser),
        ("albums-by-genre", "Albums by Genre", AlbumsByGenreBrowser),
        ("albums-by-year", "Albums by Year", AlbumsByYearBrowser),
    ]

    def __init__(self, *args, **kwargs):
        super(BeetsLibraryProvider, self).__init__(*args, **kwargs)
        self.remote = self.backend.beets_api
        self.category_browsers = []
        for key, label, browser_class in self.root_categorie_list:
            ref = models.Ref.directory(
                name=label, uri=assemble_uri(self.root_directory.uri, key)
            )
            browser = browser_class(ref, self.remote)
            self.category_browsers.append(browser)

    def browse(self, uri):
        logger.debug("Browsing Beets at: %s", uri)
        path, item_id = parse_uri(uri, uri_prefix=self.root_directory.uri)
        if path is None:
            logger.error("Beets - failed to parse uri: %s", uri)
            return []
        elif uri == self.root_directory.uri:
            # top level - show the categories
            refs = [browser.ref for browser in self.category_browsers]
            refs.sort(key=lambda item: item.name)
            return refs
        elif path == "album":
            # show an album
            try:
                album_id = int(item_id)
            except ValueError:
                logger.error("Beets - invalid album ID in URI: %s", uri)
                return []
            tracks = self.remote.get_tracks_by(
                [("album_id", album_id)], True, ["track+"]
            )
            return [
                models.Ref.track(uri=track.uri, name=track.name)
                for track in tracks
            ]
        else:
            # show a generic category directory
            for browser in self.category_browsers:
                if (
                    path
                    == parse_uri(
                        browser.ref.uri, uri_prefix=self.root_directory.uri
                    )[0]
                ):
                    if item_id is None:
                        return browser.get_toplevel()
                    else:
                        return browser.get_directory(item_id)
            else:
                logger.error("Beets - Invalid browse URI: %s / %s", uri, path)
                return []

    def search(self, query=None, uris=None, exact=False):
        # TODO: restrict the result to 'uris'
        logger.debug(
            'Beets Query (exact=%s) within "%s": %s', exact, uris, query
        )
        if not self.remote.has_connection:
            return SearchResult(uri="beets:search-disconnected", tracks=[])

        self._validate_query(query)
        search_list = []
        for (field, values) in query.items():
            for val in values:
                # missing / unsupported fields: uri, performer
                if field == "any":
                    search_list.append(val)
                elif field == "album":
                    search_list.append(("album", val))
                elif field == "artist":
                    search_list.append(("artist", val))
                elif field == "albumartist":
                    search_list.append(("albumartist", val))
                elif field == "track_name":
                    search_list.append(("title", val))
                elif field == "track_no":
                    search_list.append(("track", val))
                elif field == "composer":
                    search_list.append(("composer", val))
                elif field == "genre":
                    search_list.append(("genre", val))
                elif field == "comment":
                    search_list.append(("comments", val))
                elif field == "date":
                    # supported date formats: YYYY, YYYY-MM, YYYY-MM-DD
                    # Days and months may consist of one or two digits.
                    # A slash (instead of a dash) is acceptable as a separator.
                    match = DATE_REGEX.search(val)
                    if match:
                        # remove None values
                        for key, value in match.groupdict().items():
                            if value:
                                search_list.append((key, int(value)))
                    else:
                        logger.info(
                            "Beets search: ignoring unknown date format (%s). "
                            'It should be "YYYY", "YYYY-MM" or "YYYY-MM-DD".',
                            val,
                        )
                else:
                    logger.info("Beets: ignoring unknown query key: %s", field)
                    break
        logger.debug("Beets search query: %s", search_list)
        tracks = self.remote.get_tracks_by(search_list, exact, [])
        uri = "-".join(
            [
                item if isinstance(item, str) else "=".join(item)
                for item in search_list
            ]
        )
        return SearchResult(uri="beets:search-" + uri, tracks=tracks)

    def lookup(self, uri=None, uris=None):
        logger.debug("Beets lookup: %s", uri or uris)
        if uri:
            # the older method (mopidy < 1.0): return a list of tracks
            # handle one or more tracks given with multiple semicolons
            logger.debug("Beets lookup: %s", uri)
            path, item_id = parse_uri(uri, uri_prefix=self.root_directory.uri)
            if path == "track":
                tracks = [self.remote.get_track(item_id)]
            elif path == "album":
                tracks = self.remote.get_tracks_by(
                    [("album_id", item_id)], True, ("disc+", "track+")
                )
            elif path == "artist":
                artist_tracks = self.remote.get_tracks_by(
                    [("artist", item_id)], True, []
                )
                composer_tracks = self.remote.get_tracks_by(
                    [("composer", item_id)], True, []
                )
                # Append composer tracks to the artist tracks (unique items).
                tracks = list(set(artist_tracks + composer_tracks))
                tracks.sort(key=lambda t: (t.date, t.disc_no, t.track_no))
            else:
                logger.info("Unknown Beets lookup URI: %s", uri)
                tracks = []
            # remove occourences of None
            return [track for track in tracks if track]
        else:
            # the newer method (mopidy>=1.0): return a dict of uris and tracks
            return {uri: self.lookup(uri=uri) for uri in uris}

    def get_distinct(self, field, query=None):
        logger.debug("Beets distinct query: %s (uri=%s)", field, query)
        if not self.remote.has_connection:
            return []
        else:
            return self.remote.get_sorted_unique_track_attributes(field)

    def _validate_query(self, query):
        for values in query.values():
            if not values:
                raise LookupError("Missing query")
            for value in values:
                if not value:
                    raise LookupError("Missing query")