Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import abc
2import copy
3import hashlib
4import uuid
5from dataclasses import dataclass
6from datetime import timedelta
7from enum import Enum
8from pathlib import Path
9from typing import (
10 Any,
11 cast,
12 Dict,
13 Iterable,
14 List,
15 Optional,
16 Sequence,
17 Set,
18 Tuple,
19)
21import gi
23gi.require_version("Gtk", "3.0")
24from gi.repository import Gtk
26try:
27 import keyring
29 keyring_imported = True
30except Exception:
31 keyring_imported = False
33from .api_objects import (
34 Album,
35 Artist,
36 Directory,
37 Genre,
38 Playlist,
39 PlayQueue,
40 SearchResult,
41 Song,
42)
43from ..util import this_decade
46class SongCacheStatus(Enum):
47 """
48 Represents the cache state of a given song.
50 * :class:`SongCacheStatus.NOT_CACHED` -- indicates that the song is not cached on
51 disk.
52 * :class:`SongCacheStatus.CACHED` -- indicates that the song is cached on disk.
53 * :class:`SongCacheStatus.PERMANENTLY_CACHED` -- indicates that the song is cached
54 on disk and will not be deleted when the cache gets too big.
55 * :class:`SongCacheStatus.DOWNLOADING` -- indicates that the song is being
56 downloaded.
57 * :class:`SongCacheStatus.CACHED_STALE` -- indicates that the song is cached on
58 disk, but has been invalidated.
59 """
61 NOT_CACHED = 0
62 CACHED = 1
63 PERMANENTLY_CACHED = 2
64 DOWNLOADING = 3
65 CACHED_STALE = 4
68@dataclass
69class AlbumSearchQuery:
70 """
71 Represents a query for getting albums from an adapter. The UI will request the
72 albums in pages.
74 **Fields:**
76 * :class:`AlbumSearchQuery.type` -- the query :class:`AlbumSearchQuery.Type`
77 * :class:`AlbumSearchQuery.year_range` -- (guaranteed to only exist if ``type`` is
78 :class:`AlbumSearchQuery.Type.YEAR_RANGE`) a tuple with the lower and upper bound
79 (inclusive) of the album years to return
80 * :class:`AlbumSearchQuery.genre` -- (guaranteed to only exist if the ``type`` is
81 :class:`AlbumSearchQuery.Type.GENRE`) return albums of the given genre
82 """
84 class _Genre(Genre):
85 def __init__(self, name: str):
86 self.name = name
88 class Type(Enum):
89 """
90 Represents a type of query. Use :class:`Adapter.supported_artist_query_types` to
91 specify what search types your adapter supports.
93 * :class:`AlbumSearchQuery.Type.RANDOM` -- return a random set of albums
94 * :class:`AlbumSearchQuery.Type.NEWEST` -- return the most recently added albums
95 * :class:`AlbumSearchQuery.Type.RECENT` -- return the most recently played
96 albums
97 * :class:`AlbumSearchQuery.Type.STARRED` -- return only starred albums
98 * :class:`AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME` -- return the albums
99 sorted alphabetically by album name
100 * :class:`AlbumSearchQuery.Type.ALPHABETICAL_BY_ARTIST` -- return the albums
101 sorted alphabetically by artist name
102 * :class:`AlbumSearchQuery.Type.YEAR_RANGE` -- return albums in the given year
103 range
104 * :class:`AlbumSearchQuery.Type.GENRE` -- return songs of the given genre
105 """
107 RANDOM = 0
108 NEWEST = 1
109 FREQUENT = 2
110 RECENT = 3
111 STARRED = 4
112 ALPHABETICAL_BY_NAME = 5
113 ALPHABETICAL_BY_ARTIST = 6
114 YEAR_RANGE = 7
115 GENRE = 8
117 type: Type
118 year_range: Tuple[int, int] = this_decade()
119 genre: Genre = _Genre("Rock")
121 _strhash: Optional[str] = None
123 def strhash(self) -> str:
124 """
125 Returns a deterministic hash of the query as a string.
127 >>> AlbumSearchQuery(
128 ... AlbumSearchQuery.Type.YEAR_RANGE, year_range=(2018, 2019)
129 ... ).strhash()
130 '275c58cac77c5ea9ccd34ab870f59627ab98e73c'
131 >>> AlbumSearchQuery(
132 ... AlbumSearchQuery.Type.YEAR_RANGE, year_range=(2018, 2020)
133 ... ).strhash()
134 'e5dc424e8fc3b7d9ff7878b38cbf2c9fbdc19ec2'
135 >>> AlbumSearchQuery(AlbumSearchQuery.Type.STARRED).strhash()
136 '861b6ff011c97d53945ca89576463d0aeb78e3d2'
137 """
138 if not self._strhash:
139 hash_tuple: Tuple[Any, ...] = (self.type.value,)
140 if self.type == AlbumSearchQuery.Type.YEAR_RANGE:
141 hash_tuple += (self.year_range,)
142 elif self.type == AlbumSearchQuery.Type.GENRE:
143 hash_tuple += (self.genre.name,)
144 self._strhash = hashlib.sha1(bytes(str(hash_tuple), "utf8")).hexdigest()
145 return self._strhash
148class CacheMissError(Exception):
149 """
150 This exception should be thrown by caching adapters when the request data is not
151 available or is invalid. If some of the data is available, but not all of it, the
152 ``partial_data`` parameter should be set with the partial data. If the ground truth
153 adapter can't service the request, or errors for some reason, the UI will try to
154 populate itself with the partial data returned in this exception (with the necessary
155 error text to inform the user that retrieval from the ground truth adapter failed).
156 """
158 def __init__(self, *args, partial_data: Any = None):
159 """
160 Create a :class:`CacheMissError` exception.
162 :param args: arguments to pass to the :class:`BaseException` base class.
163 :param partial_data: the actual partial data for the UI to use in case of ground
164 truth adapter failure.
165 """
166 self.partial_data = partial_data
167 super().__init__(*args)
170KEYRING_APP_NAME = "app.sublimemusic.SublimeMusic"
173class ConfigurationStore(dict):
174 """
175 This defines an abstract store for all configuration parameters for a given Adapter.
176 """
178 MOCK = False
180 def __init__(self, **kwargs):
181 super().__init__(**kwargs)
182 self._changed_secrets_store = {}
184 def __repr__(self) -> str:
185 values = ", ".join(f"{k}={v!r}" for k, v in sorted(self.items()))
186 return f"ConfigurationStore({values})"
188 def clone(self) -> "ConfigurationStore":
189 configuration_store = ConfigurationStore(**copy.deepcopy(self))
190 configuration_store._changed_secrets_store = copy.deepcopy(
191 self._changed_secrets_store
192 )
193 return configuration_store
195 def persist_secrets(self):
196 if not keyring_imported or ConfigurationStore.MOCK:
197 return
199 for key, secret in self._changed_secrets_store.items():
200 try:
201 password_id = None
202 if password_type_and_id := self.get(key):
203 if cast(List[str], password_type_and_id)[0] == "keyring":
204 password_id = password_type_and_id[1]
206 if password_id is None:
207 password_id = str(uuid.uuid4())
209 keyring.set_password(KEYRING_APP_NAME, password_id, secret)
210 self[key] = ["keyring", password_id]
211 except Exception:
212 return
214 def get_secret(self, key: str) -> Optional[str]:
215 """
216 Get the secret value in the store with the given key. This will retrieve the
217 secret from whatever is configured as the underlying secret storage mechanism so
218 you don't have to deal with secret storage yourself.
219 """
220 if secret := self._changed_secrets_store.get(key):
221 return secret
223 value = self.get(key)
224 if not isinstance(value, list) or len(value) != 2:
225 return None
227 storage_type, storage_key = value
228 return {
229 "keyring": lambda: keyring.get_password(KEYRING_APP_NAME, storage_key),
230 "plaintext": lambda: storage_key,
231 }[storage_type]()
233 def set_secret(self, key: str, value: str = None):
234 """
235 Set the secret value of the given key in the store. This should be used for
236 things such as passwords or API tokens. When :class:`persist_secrets` is called,
237 the secrets will be stored in whatever is configured as the underlying secret
238 storage mechanism so you don't have to deal with secret storage yourself.
239 """
240 self._changed_secrets_store[key] = value
241 self[key] = ["plaintext", value]
244@dataclass
245class UIInfo:
246 name: str
247 description: str
248 icon_basename: str
249 icon_dir: Optional[Path] = None
251 def icon_name(self) -> str:
252 return f"{self.icon_basename}-symbolic"
254 def status_icon_name(self, status: str) -> str:
255 return f"{self.icon_basename}-{status.lower()}-symbolic"
258class Adapter(abc.ABC):
259 """
260 Defines the interface for a Sublime Music Adapter.
262 All functions that actually retrieve data have a corresponding: ``can_``-prefixed
263 property (which can be dynamic) which specifies whether or not the adapter supports
264 that operation at the moment.
265 """
267 # Configuration and Initialization Properties
268 # These functions determine how the adapter can be configured and how to
269 # initialize the adapter given those configuration values.
270 # ==================================================================================
271 @staticmethod
272 @abc.abstractmethod
273 def get_ui_info() -> UIInfo:
274 """
275 :returns: A :class:`UIInfo` object.
276 """
278 @staticmethod
279 @abc.abstractmethod
280 def get_configuration_form(config_store: ConfigurationStore) -> Gtk.Box:
281 """
282 This function should return a :class:`Gtk.Box` that gets any inputs required
283 from the user and uses the given ``config_store`` to store the configuration
284 values.
286 The ``Gtk.Box`` must expose a signal with the name ``"config-valid-changed"``
287 which returns a single boolean value indicating whether or not the configuration
288 is valid.
290 If you don't want to implement all of the GTK logic yourself, and just want a
291 simple form, then you can use the :class:`ConfigureServerForm` class to generate
292 a form in a declarative manner.
293 """
295 @staticmethod
296 @abc.abstractmethod
297 def migrate_configuration(config_store: ConfigurationStore):
298 """
299 This function allows the adapter to migrate its configuration.
300 """
302 @abc.abstractmethod
303 def __init__(self, config_store: ConfigurationStore, data_directory: Path):
304 """
305 This function should be overridden by inheritors of :class:`Adapter` and should
306 be used to do whatever setup is required for the adapter.
308 This should do the bare minimum to get things set up, since this blocks the main
309 UI loop. If you need to do longer initialization, use the :class:`initial_sync`
310 function.
312 :param config: The adapter configuration. The keys of are the configuration
313 parameter names as defined by the return value of the
314 :class:`get_config_parameters` function. The values are the actual value of
315 the configuration parameter.
316 :param data_directory: the directory where the adapter can store data. This
317 directory is guaranteed to exist.
318 """
320 @abc.abstractmethod
321 def initial_sync(self):
322 """
323 Perform any operations that are required to get the adapter functioning
324 properly. For example, this function can be used to wait for an initial ping to
325 come back from the server.
326 """
328 @abc.abstractmethod
329 def shutdown(self):
330 """
331 This function is called when the app is being closed or the server is changing.
332 This should be used to clean up anything that is necessary such as writing a
333 cache to disk, disconnecting from a server, etc.
334 """
336 # Usage Properties
337 # These properties determine how the adapter can be used and how quickly
338 # data can be expected from this adapter.
339 # ==================================================================================
340 @property
341 def can_be_cached(self) -> bool:
342 """
343 Whether or not this adapter can be used as the ground-truth adapter behind a
344 caching adapter.
346 The default is ``True``, since most adapters will want to take advantage of the
347 built-in filesystem cache.
348 """
349 return True
351 @property
352 @staticmethod
353 def can_be_ground_truth() -> bool:
354 """
355 Whether or not this adapter can be used as a ground truth adapter.
356 """
357 return True
359 # Network Properties
360 # These properties determine whether or not the adapter requires connection over a
361 # network and whether the underlying server can be pinged.
362 # ==================================================================================
363 @property
364 def is_networked(self) -> bool:
365 """
366 Whether or not this adapter operates over the network. This will be used to
367 determine whether or not some of the offline/online management features should
368 be enabled.
369 """
370 return True
372 def on_offline_mode_change(self, offline_mode: bool):
373 """
374 This function should be used to handle any operations that need to be performed
375 when Sublime Music goes from online to offline mode or vice versa.
376 """
378 @property
379 @abc.abstractmethod
380 def ping_status(self) -> bool:
381 """
382 If the adapter :class:`is_networked`, then this function should return whether
383 or not the server can be pinged. This function must provide an answer
384 *instantly* (it can't actually ping the server). This function is called *very*
385 often, and even a few milliseconds delay stacks up quickly and can block the UI
386 thread.
388 One option is to ping the server every few seconds and cache the result of the
389 ping and use that as the result of this function.
390 """
392 # Availability Properties
393 # These properties determine if what things the adapter can be used to do. These
394 # properties can be dynamic based on things such as underlying API version, or other
395 # factors like that. However, these properties should not be dependent on the
396 # connection state. After the initial sync, these properties are expected to be
397 # constant.
398 # ==================================================================================
399 # Playlists
400 @property
401 def can_get_playlists(self) -> bool:
402 """
403 Whether or not the adapter supports :class:`get_playlist`.
404 """
405 return False
407 @property
408 def can_get_playlist_details(self) -> bool:
409 """
410 Whether or not the adapter supports :class:`get_playlist_details`.
411 """
412 return False
414 @property
415 def can_create_playlist(self) -> bool:
416 """
417 Whether or not the adapter supports :class:`create_playlist`.
418 """
419 return False
421 @property
422 def can_update_playlist(self) -> bool:
423 """
424 Whether or not the adapter supports :class:`update_playlist`.
425 """
426 return False
428 @property
429 def can_delete_playlist(self) -> bool:
430 """
431 Whether or not the adapter supports :class:`delete_playlist`.
432 """
433 return False
435 # Downloading/streaming cover art and songs
436 @property
437 def supported_schemes(self) -> Iterable[str]:
438 """
439 Specifies a collection of scheme names that can be provided by the adapter for a
440 given resource (song or cover art) right now.
442 Examples of values that could be provided include ``http``, ``https``, ``file``,
443 or ``ftp``.
444 """
445 return ()
447 @property
448 def can_get_cover_art_uri(self) -> bool:
449 """
450 Whether or not the adapter supports :class:`get_cover_art_uri`.
451 """
452 return False
454 @property
455 def can_get_song_file_uri(self) -> bool:
456 """
457 Whether or not the adapter supports :class:`get_song_file_uri`.
458 """
459 return False
461 @property
462 def can_get_song_stream_uri(self) -> bool:
463 """
464 Whether or not the adapter supports :class:`get_song_stream_uri`.
465 """
466 return False
468 # Songs
469 @property
470 def can_get_song_details(self) -> bool:
471 """
472 Whether or not the adapter supports :class:`get_song_details`.
473 """
474 return False
476 @property
477 def can_scrobble_song(self) -> bool:
478 """
479 Whether or not the adapter supports :class:`scrobble_song`.
480 """
481 return False
483 # Artists
484 @property
485 def supported_artist_query_types(self) -> Set[AlbumSearchQuery.Type]:
486 """
487 A set of the query types that this adapter can service.
489 :returns: A set of :class:`AlbumSearchQuery.Type` objects.
490 """
491 return set()
493 @property
494 def can_get_artists(self) -> bool:
495 """
496 Whether or not the adapter supports :class:`get_aritsts`.
497 """
498 return False
500 @property
501 def can_get_artist(self) -> bool:
502 """
503 Whether or not the adapter supports :class:`get_aritst`.
504 """
505 return False
507 @property
508 def can_get_ignored_articles(self) -> bool:
509 """
510 Whether or not the adapter supports :class:`get_ignored_articles`.
511 """
512 return False
514 # Albums
515 @property
516 def can_get_albums(self) -> bool:
517 """
518 Whether or not the adapter supports :class:`get_albums`.
519 """
520 return False
522 @property
523 def can_get_album(self) -> bool:
524 """
525 Whether or not the adapter supports :class:`get_album`.
526 """
527 return False
529 # Browse directories
530 @property
531 def can_get_directory(self) -> bool:
532 """
533 Whether or not the adapter supports :class:`get_directory`.
534 """
535 return False
537 # Genres
538 @property
539 def can_get_genres(self) -> bool:
540 """
541 Whether or not the adapter supports :class:`get_genres`.
542 """
543 return False
545 # Play Queue
546 @property
547 def can_get_play_queue(self) -> bool:
548 """
549 Whether or not the adapter supports :class:`get_play_queue`.
550 """
551 return False
553 @property
554 def can_save_play_queue(self) -> bool:
555 """
556 Whether or not the adapter supports :class:`save_play_queue`.
557 """
558 return False
560 # Search
561 @property
562 def can_search(self) -> bool:
563 """
564 Whether or not the adapter supports :class:`search`.
565 """
566 return False
568 # Data Retrieval Methods
569 # These properties determine if what things the adapter can be used to do
570 # at the current moment.
571 # ==================================================================================
572 def get_playlists(self) -> Sequence[Playlist]:
573 """
574 Get a list of all of the playlists known by the adapter.
576 :returns: A list of all of the
577 :class:`sublime_music.adapter.api_objects.Playlist` objects known to the
578 adapter.
579 """
580 raise self._check_can_error("get_playlists")
582 def get_playlist_details(self, playlist_id: str) -> Playlist:
583 """
584 Get the details for the given ``playlist_id``. If the playlist_id does not
585 exist, then this function should throw an exception.
587 :param playlist_id: The ID of the playlist to retrieve.
588 :returns: A :class:`sublime_music.adapter.api_objects.Play` object for the given
589 playlist.
590 """
591 raise self._check_can_error("get_playlist_details")
593 def create_playlist(
594 self, name: str, songs: Sequence[Song] = None
595 ) -> Optional[Playlist]:
596 """
597 Creates a playlist of the given name with the given songs.
599 :param name: The human-readable name of the playlist.
600 :param songs: A list of songs that should be included in the playlist.
601 :returns: A :class:`sublime_music.adapter.api_objects.Playlist` object for the
602 created playlist. If getting this information will incurr network overhead,
603 then just return ``None``.
604 """
605 raise self._check_can_error("create_playlist")
607 def update_playlist(
608 self,
609 playlist_id: str,
610 name: str = None,
611 comment: str = None,
612 public: bool = None,
613 song_ids: Sequence[str] = None,
614 ) -> Playlist:
615 """
616 Updates a given playlist. If a parameter is ``None``, then it will be ignored
617 and no updates will occur to that field.
619 :param playlist_id: The human-readable name of the playlist.
620 :param name: The human-readable name of the playlist.
621 :param comment: The playlist comment.
622 :param public: This is very dependent on the adapter, but if the adapter has a
623 shared/public vs. not shared/private playlists concept, setting this to
624 ``True`` will make the playlist shared/public.
625 :param song_ids: A list of song IDs that should be included in the playlist.
626 :returns: A :class:`sublime_music.adapter.api_objects.Playlist` object for the
627 updated playlist.
628 """
629 raise self._check_can_error("update_playlist")
631 def delete_playlist(self, playlist_id: str):
632 """
633 Deletes the given playlist.
635 :param playlist_id: The human-readable name of the playlist.
636 """
637 raise self._check_can_error("delete_playlist")
639 def get_cover_art_uri(self, cover_art_id: str, scheme: str, size: int) -> str:
640 """
641 Get a URI for a given ``cover_art_id``.
643 :param cover_art_id: The song, album, or artist ID.
644 :param scheme: The URI scheme that should be returned. It is guaranteed that
645 ``scheme`` will be one of the schemes returned by
646 :class:`supported_schemes`.
647 :param size: The size of image to return. Denotes the max width or max height
648 (whichever is larger).
649 :returns: The URI as a string.
650 """
651 raise self._check_can_error("get_cover_art_uri")
653 def get_song_file_uri(self, song_id: str, schemes: Iterable[str]) -> str:
654 """
655 Get a URI for a given song. This URI must give the full file.
657 :param song_id: The ID of the song to get a URI for.
658 :param schemes: A set of URI schemes that can be returned. It is guaranteed that
659 all of the items in ``schemes`` will be one of the schemes returned by
660 :class:`supported_schemes`.
661 :returns: The URI for the given song.
662 """
663 raise self._check_can_error("get_song_file_uri")
665 def get_song_stream_uri(self, song_id: str) -> str:
666 """
667 Get a URI for streaming the given song.
669 :param song_id: The ID of the song to get the stream URI for.
670 :returns: the stream URI for the given song.
671 """
672 raise self._check_can_error("get_song_stream_uri")
674 def get_song_details(self, song_id: str) -> Song:
675 """
676 Get the details for a given song ID.
678 :param song_id: The ID of the song to get the details for.
679 :returns: The :class:`sublime_music.adapters.api_objects.Song`.
680 """
681 raise self._check_can_error("get_song_details")
683 def scrobble_song(self, song: Song):
684 """
685 Scrobble the given song.
687 :params song: The :class:`sublime_music.adapters.api_objects.Song` to scrobble.
688 """
689 raise self._check_can_error("scrobble_song")
691 def get_artists(self) -> Sequence[Artist]:
692 """
693 Get a list of all of the artists known to the adapter.
695 :returns: A list of all of the :class:`sublime_music.adapter.api_objects.Artist`
696 objects known to the adapter.
697 """
698 raise self._check_can_error("get_artists")
700 def get_artist(self, artist_id: str) -> Artist:
701 """
702 Get the details for the given artist ID.
704 :param artist_id: The ID of the artist to get the details for.
705 :returns: The :classs`sublime_music.adapters.api_objects.Artist`
706 """
707 raise self._check_can_error("get_artist")
709 def get_ignored_articles(self) -> Set[str]:
710 """
711 Get the list of articles to ignore when sorting artists by name.
713 :returns: A set of articles (i.e. The, A, El, La, Los) to ignore when sorting
714 artists.
715 """
716 raise self._check_can_error("get_ignored_articles")
718 def get_albums(
719 self, query: AlbumSearchQuery, sort_direction: str = "ascending"
720 ) -> Sequence[Album]:
721 """
722 Get a list of all of the albums known to the adapter for the given query.
724 .. note::
726 This request is not paged. You should do any page management to get all of
727 the albums matching the query internally.
729 :param query: An :class:`AlbumSearchQuery` object representing the types of
730 albums to return.
731 :returns: A list of all of the :class:`sublime_music.adapter.api_objects.Album`
732 objects known to the adapter that match the query.
733 """
734 raise self._check_can_error("get_albums")
736 def get_album(self, album_id: str) -> Album:
737 """
738 Get the details for the given album ID.
740 :param album_id: The ID of the album to get the details for.
741 :returns: The :classs`sublime_music.adapters.api_objects.Album`
742 """
743 raise self._check_can_error("get_album")
745 def get_directory(self, directory_id: str) -> Directory:
746 """
747 Return a Directory object representing the song files and directories in the
748 given directory. This may not make sense for your adapter (for example, if
749 there's no actual underlying filesystem). In that case, make sure to set
750 :class:`can_get_directory` to ``False``.
752 :param directory_id: The directory to retrieve. If the special value ``"root"``
753 is given, the adapter should list all of the directories at the root of the
754 filesystem tree.
755 :returns: A list of the :class:`sublime_music.adapter.api_objects.Directory` and
756 :class:`sublime_music.adapter.api_objects.Song` objects in the given
757 directory.
758 """
759 raise self._check_can_error("get_directory")
761 def get_genres(self) -> Sequence[Genre]:
762 """
763 Get a list of the genres known to the adapter.
765 :returns: A list of all of the :classs`sublime_music.adapter.api_objects.Genre`
766 objects known to the adapter.
767 """
768 raise self._check_can_error("get_genres")
770 def get_play_queue(self) -> Optional[PlayQueue]:
771 """
772 Returns the state of the play queue for this user. This could be used to restore
773 the play queue from the cloud.
775 :returns: The cloud-saved play queue as a
776 :class:`sublime_music.adapter.api_objects.PlayQueue` object.
777 """
778 raise self._check_can_error("get_play_queue")
780 def save_play_queue(
781 self,
782 song_ids: Sequence[str],
783 current_song_index: int = None,
784 position: timedelta = None,
785 ):
786 """
787 Save the current play queue to the cloud.
789 :param song_ids: A list of the song IDs in the queue.
790 :param current_song_index: The index of the song that is currently being played.
791 :param position: The current position in the song.
792 """
793 raise self._check_can_error("can_save_play_queue")
795 def search(self, query: str) -> SearchResult:
796 """
797 Return search results fro the given query.
799 :param query: The query string.
800 :returns: A :class:`sublime_music.adapters.api_objects.SearchResult` object
801 representing the results of the search.
802 """
803 raise self._check_can_error("search")
805 @staticmethod
806 def _check_can_error(method_name: str) -> NotImplementedError:
807 return NotImplementedError(
808 f"Adapter.{method_name} called. "
809 "Did you forget to check that can_{method_name} is True?"
810 )
813class CachingAdapter(Adapter):
814 """
815 Defines an adapter that can be used as a cache for another adapter.
817 A caching adapter sits "in front" of a non-caching adapter and the UI will attempt
818 to retrieve the data from the caching adapter before retrieving it from the
819 non-caching adapter. (The exception is when the UI requests that the data come
820 directly from the ground truth adapter, in which case the cache will be bypassed.)
822 Caching adapters *must* be able to service requests instantly, or nearly instantly
823 (in most cases, this means that the data must come directly from the local
824 filesystem).
825 """
827 @abc.abstractmethod
828 def __init__(self, config: dict, data_directory: Path, is_cache: bool = False):
829 """
830 This function should be overridden by inheritors of :class:`CachingAdapter` and
831 should be used to do whatever setup is required for the adapter.
833 :param config: The adapter configuration. The keys of are the configuration
834 parameter names as defined by the return value of the
835 :class:`get_config_parameters` function. The values are the actual value of
836 the configuration parameter.
837 :param data_directory: the directory where the adapter can store data. This
838 directory is guaranteed to exist.
839 :param is_cache: whether or not the adapter is being used as a cache.
840 """
842 ping_status = True
844 # Data Ingestion Methods
845 # ==================================================================================
846 class CachedDataKey(Enum):
847 ALBUM = "album"
848 ALBUMS = "albums"
849 ARTIST = "artist"
850 ARTISTS = "artists"
851 COVER_ART_FILE = "cover_art_file"
852 DIRECTORY = "directory"
853 GENRE = "genre"
854 GENRES = "genres"
855 IGNORED_ARTICLES = "ignored_articles"
856 PLAYLIST_DETAILS = "get_playlist_details"
857 PLAYLISTS = "get_playlists"
858 SEARCH_RESULTS = "search_results"
859 SONG = "song"
860 SONG_FILE = "song_file"
861 SONG_FILE_PERMANENT = "song_file_permanent"
863 # These are only for clearing the cache, and will only do deletion
864 ALL_SONGS = "all_songs"
865 EVERYTHING = "everything"
867 @abc.abstractmethod
868 def ingest_new_data(self, data_key: CachedDataKey, param: Optional[str], data: Any):
869 """
870 This function will be called after the fallback, ground-truth adapter returns
871 new data. This normally will happen if this adapter has a cache miss or if the
872 UI forces retrieval from the ground-truth adapter.
874 :param data_key: the type of data to be ingested.
875 :param param: a string that uniquely identify the data to be ingested. For
876 example, with playlist details, this will be the playlist ID. If that
877 playlist ID is requested again, the adapter should service that request, but
878 it should not service a request for a different playlist ID.
880 For the playlist list, this will be none since there are no parameters to
881 that request.
882 :param data: the data that was returned by the ground truth adapter.
883 """
885 @abc.abstractmethod
886 def invalidate_data(self, data_key: CachedDataKey, param: Optional[str]):
887 """
888 This function will be called if the adapter should invalidate some of its data.
889 This should not destroy the invalidated data. If invalid data is requested, a
890 ``CacheMissError`` should be thrown, but the old data should be included in the
891 ``partial_data`` field of the error.
893 :param data_key: the type of data to be invalidated.
894 :param params: the parameters that uniquely identify the data to be invalidated.
895 For example, with playlist details, this will be the playlist ID.
897 For the playlist list, this will be none since there are no parameters to
898 that request.
899 """
901 @abc.abstractmethod
902 def delete_data(self, data_key: CachedDataKey, param: Optional[str]):
903 """
904 This function will be called if the adapter should delete some of its data.
905 This should destroy the data. If the deleted data is requested, a
906 ``CacheMissError`` should be thrown with no data in the ``partial_data`` field.
908 :param data_key: the type of data to be deleted.
909 :param params: the parameters that uniquely identify the data to be invalidated.
910 For example, with playlist details, this will be the playlist ID.
912 For the playlist list, this will be none since there are no parameters to
913 that request.
914 """
916 # Cache-Specific Methods
917 # ==================================================================================
918 @abc.abstractmethod
919 def get_cached_statuses(
920 self, song_ids: Sequence[str]
921 ) -> Dict[str, SongCacheStatus]:
922 """
923 Returns the cache statuses for the given list of songs. See the
924 :class:`SongCacheStatus` documentation for more details about what each status
925 means.
927 :params songs: The songs to get the cache status for.
928 :returns: A dictionary of song ID to :class:`SongCacheStatus` objects for each
929 of the songs.
930 """