Hide keyboard shortcuts

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) 

20 

21import gi 

22 

23gi.require_version("Gtk", "3.0") 

24from gi.repository import Gtk 

25 

26try: 

27 import keyring 

28 

29 keyring_imported = True 

30except Exception: 

31 keyring_imported = False 

32 

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 

44 

45 

46class SongCacheStatus(Enum): 

47 """ 

48 Represents the cache state of a given song. 

49 

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 """ 

60 

61 NOT_CACHED = 0 

62 CACHED = 1 

63 PERMANENTLY_CACHED = 2 

64 DOWNLOADING = 3 

65 CACHED_STALE = 4 

66 

67 

68@dataclass 

69class AlbumSearchQuery: 

70 """ 

71 Represents a query for getting albums from an adapter. The UI will request the 

72 albums in pages. 

73 

74 **Fields:** 

75 

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 """ 

83 

84 class _Genre(Genre): 

85 def __init__(self, name: str): 

86 self.name = name 

87 

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. 

92 

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 """ 

106 

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 

116 

117 type: Type 

118 year_range: Tuple[int, int] = this_decade() 

119 genre: Genre = _Genre("Rock") 

120 

121 _strhash: Optional[str] = None 

122 

123 def strhash(self) -> str: 

124 """ 

125 Returns a deterministic hash of the query as a string. 

126 

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 

146 

147 

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 """ 

157 

158 def __init__(self, *args, partial_data: Any = None): 

159 """ 

160 Create a :class:`CacheMissError` exception. 

161 

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) 

168 

169 

170KEYRING_APP_NAME = "app.sublimemusic.SublimeMusic" 

171 

172 

173class ConfigurationStore(dict): 

174 """ 

175 This defines an abstract store for all configuration parameters for a given Adapter. 

176 """ 

177 

178 MOCK = False 

179 

180 def __init__(self, **kwargs): 

181 super().__init__(**kwargs) 

182 self._changed_secrets_store = {} 

183 

184 def __repr__(self) -> str: 

185 values = ", ".join(f"{k}={v!r}" for k, v in sorted(self.items())) 

186 return f"ConfigurationStore({values})" 

187 

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 

194 

195 def persist_secrets(self): 

196 if not keyring_imported or ConfigurationStore.MOCK: 

197 return 

198 

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] 

205 

206 if password_id is None: 

207 password_id = str(uuid.uuid4()) 

208 

209 keyring.set_password(KEYRING_APP_NAME, password_id, secret) 

210 self[key] = ["keyring", password_id] 

211 except Exception: 

212 return 

213 

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 

222 

223 value = self.get(key) 

224 if not isinstance(value, list) or len(value) != 2: 

225 return None 

226 

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]() 

232 

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] 

242 

243 

244@dataclass 

245class UIInfo: 

246 name: str 

247 description: str 

248 icon_basename: str 

249 icon_dir: Optional[Path] = None 

250 

251 def icon_name(self) -> str: 

252 return f"{self.icon_basename}-symbolic" 

253 

254 def status_icon_name(self, status: str) -> str: 

255 return f"{self.icon_basename}-{status.lower()}-symbolic" 

256 

257 

258class Adapter(abc.ABC): 

259 """ 

260 Defines the interface for a Sublime Music Adapter. 

261 

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 """ 

266 

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 """ 

277 

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. 

285 

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. 

289 

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 """ 

294 

295 @staticmethod 

296 @abc.abstractmethod 

297 def migrate_configuration(config_store: ConfigurationStore): 

298 """ 

299 This function allows the adapter to migrate its configuration. 

300 """ 

301 

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. 

307 

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. 

311 

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 """ 

319 

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 """ 

327 

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 """ 

335 

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. 

345 

346 The default is ``True``, since most adapters will want to take advantage of the 

347 built-in filesystem cache. 

348 """ 

349 return True 

350 

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 

358 

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 

371 

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 """ 

377 

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. 

387 

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 """ 

391 

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 

406 

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 

413 

414 @property 

415 def can_create_playlist(self) -> bool: 

416 """ 

417 Whether or not the adapter supports :class:`create_playlist`. 

418 """ 

419 return False 

420 

421 @property 

422 def can_update_playlist(self) -> bool: 

423 """ 

424 Whether or not the adapter supports :class:`update_playlist`. 

425 """ 

426 return False 

427 

428 @property 

429 def can_delete_playlist(self) -> bool: 

430 """ 

431 Whether or not the adapter supports :class:`delete_playlist`. 

432 """ 

433 return False 

434 

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. 

441 

442 Examples of values that could be provided include ``http``, ``https``, ``file``, 

443 or ``ftp``. 

444 """ 

445 return () 

446 

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 

453 

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 

460 

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 

467 

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 

475 

476 @property 

477 def can_scrobble_song(self) -> bool: 

478 """ 

479 Whether or not the adapter supports :class:`scrobble_song`. 

480 """ 

481 return False 

482 

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. 

488 

489 :returns: A set of :class:`AlbumSearchQuery.Type` objects. 

490 """ 

491 return set() 

492 

493 @property 

494 def can_get_artists(self) -> bool: 

495 """ 

496 Whether or not the adapter supports :class:`get_aritsts`. 

497 """ 

498 return False 

499 

500 @property 

501 def can_get_artist(self) -> bool: 

502 """ 

503 Whether or not the adapter supports :class:`get_aritst`. 

504 """ 

505 return False 

506 

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 

513 

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 

521 

522 @property 

523 def can_get_album(self) -> bool: 

524 """ 

525 Whether or not the adapter supports :class:`get_album`. 

526 """ 

527 return False 

528 

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 

536 

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 

544 

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 

552 

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 

559 

560 # Search 

561 @property 

562 def can_search(self) -> bool: 

563 """ 

564 Whether or not the adapter supports :class:`search`. 

565 """ 

566 return False 

567 

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. 

575 

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") 

581 

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. 

586 

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") 

592 

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. 

598 

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") 

606 

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. 

618 

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") 

630 

631 def delete_playlist(self, playlist_id: str): 

632 """ 

633 Deletes the given playlist. 

634 

635 :param playlist_id: The human-readable name of the playlist. 

636 """ 

637 raise self._check_can_error("delete_playlist") 

638 

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``. 

642 

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") 

652 

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. 

656 

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") 

664 

665 def get_song_stream_uri(self, song_id: str) -> str: 

666 """ 

667 Get a URI for streaming the given song. 

668 

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") 

673 

674 def get_song_details(self, song_id: str) -> Song: 

675 """ 

676 Get the details for a given song ID. 

677 

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") 

682 

683 def scrobble_song(self, song: Song): 

684 """ 

685 Scrobble the given song. 

686 

687 :params song: The :class:`sublime_music.adapters.api_objects.Song` to scrobble. 

688 """ 

689 raise self._check_can_error("scrobble_song") 

690 

691 def get_artists(self) -> Sequence[Artist]: 

692 """ 

693 Get a list of all of the artists known to the adapter. 

694 

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") 

699 

700 def get_artist(self, artist_id: str) -> Artist: 

701 """ 

702 Get the details for the given artist ID. 

703 

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") 

708 

709 def get_ignored_articles(self) -> Set[str]: 

710 """ 

711 Get the list of articles to ignore when sorting artists by name. 

712 

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") 

717 

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. 

723 

724 .. note:: 

725 

726 This request is not paged. You should do any page management to get all of 

727 the albums matching the query internally. 

728 

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") 

735 

736 def get_album(self, album_id: str) -> Album: 

737 """ 

738 Get the details for the given album ID. 

739 

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") 

744 

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``. 

751 

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") 

760 

761 def get_genres(self) -> Sequence[Genre]: 

762 """ 

763 Get a list of the genres known to the adapter. 

764 

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") 

769 

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. 

774 

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") 

779 

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. 

788 

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") 

794 

795 def search(self, query: str) -> SearchResult: 

796 """ 

797 Return search results fro the given query. 

798 

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") 

804 

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 ) 

811 

812 

813class CachingAdapter(Adapter): 

814 """ 

815 Defines an adapter that can be used as a cache for another adapter. 

816 

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.) 

821 

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 """ 

826 

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. 

832 

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 """ 

841 

842 ping_status = True 

843 

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" 

862 

863 # These are only for clearing the cache, and will only do deletion 

864 ALL_SONGS = "all_songs" 

865 EVERYTHING = "everything" 

866 

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. 

873 

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. 

879 

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 """ 

884 

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. 

892 

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. 

896 

897 For the playlist list, this will be none since there are no parameters to 

898 that request. 

899 """ 

900 

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. 

907 

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. 

911 

912 For the playlist list, this will be none since there are no parameters to 

913 that request. 

914 """ 

915 

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. 

926 

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 """