implemented browse_all and browse_page , should be enough for jellyplist

This commit is contained in:
Kamil
2024-11-29 22:07:11 +00:00
parent 7232b3223d
commit 25e51f1ef2

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass
import os import os
from app.providers.base import AccountAttributes, Album, Artist, Image, MusicProviderClient, Owner,Playlist, PlaylistTrack, Profile,Track,ExternalUrl,Category from app.providers.base import AccountAttributes, Album, Artist, Image, MusicProviderClient, Owner,Playlist, PlaylistTrack, Profile,Track,ExternalUrl,Category
import requests import requests
@@ -12,6 +13,21 @@ import logging
l = logging.getLogger(__name__) l = logging.getLogger(__name__)
@dataclass
class BrowseCard:
title: str
uri: str
background_color: str
artwork: List[Image]
@dataclass
class BrowseSection:
title: str
items: List[BrowseCard]
uri: str
class SpotifyClient(MusicProviderClient): class SpotifyClient(MusicProviderClient):
""" """
Spotify implementation of the MusicProviderClient. Spotify implementation of the MusicProviderClient.
@@ -225,11 +241,97 @@ class SpotifyClient(MusicProviderClient):
uri=owner_data.get("uri", ""), uri=owner_data.get("uri", ""),
external_urls=self._parse_external_urls(owner_data.get("uri", ""), "user") external_urls=self._parse_external_urls(owner_data.get("uri", ""), "user")
) )
def _parse_card_artwork(self, sources: List[Dict]) -> List[Image]:
"""
Parse artwork for a browse card.
:param sources: List of artwork source dictionaries.
:return: A list of CardArtwork instances.
"""
return [Image(url=source["url"], height=source.get("height"), width=source.get("width")) for source in sources]
def _parse_browse_card(self, card_data: Dict) -> BrowseCard:
"""
Parse a single browse card.
:param card_data: Dictionary containing card data.
:return: A BrowseCard instance.
"""
card_content = card_data["content"]["data"]["data"]["cardRepresentation"]
artwork_sources = card_content["artwork"]["sources"]
return BrowseCard(
title=card_content["title"]["transformedLabel"],
uri=card_data["uri"],
background_color=card_content["backgroundColor"]["hex"],
artwork=self._parse_card_artwork(artwork_sources)
)
def _parse_playlist(self, playlist_data: Dict) -> Playlist:
"""
Parse a playlist object from API response data.
:param playlist_data: Dictionary containing playlist data.
:return: A Playlist object.
"""
images = self._parse_images(playlist_data.get("images", {}).get("items", []))
owner_data = playlist_data.get("ownerV2", {}).get("data", {})
owner = self._parse_owner(owner_data)
tracks = [
self._parse_track(item["itemV2"]["data"])
for item in playlist_data.get("content", {}).get("items", [])
]
return Playlist(
id=playlist_data.get("uri", "").split(":")[-1],
name=playlist_data.get("name", ""),
uri=playlist_data.get("uri", ""),
external_urls=self._parse_external_urls(playlist_data.get("uri", "").split(":")[-1], "playlist"),
description=playlist_data.get("description", ""),
public=playlist_data.get("public", None),
collaborative=playlist_data.get("collaborative", None),
followers=playlist_data.get("followers", 0),
images=images,
owner=owner,
tracks=[
PlaylistTrack(
added_at=item.get("addedAt", {}).get("isoString", ""),
added_by=None,
is_local=False,
track=track
)
for item, track in zip(
playlist_data.get("content", {}).get("items", []),
tracks
)
]
)
def _parse_browse_section(self, section_data: Dict) -> BrowseSection:
"""
Parse a single browse section.
:param section_data: Dictionary containing section data.
:return: A BrowseSection instance.
"""
section_title = section_data["data"]["title"]["transformedLabel"]
section_items = [
item for item in section_data["sectionItems"]["items"]
if not item["uri"].startswith("spotify:xlink")
]
return BrowseSection(
title=section_title,
items=[self._parse_browse_card(item) for item in section_items],
uri=section_data["uri"]
)
#endregion #endregion
def get_playlist(self, playlist_id: str) -> Playlist: def get_playlist(self, playlist_id: str) -> Playlist:
""" """
Fetch a playlist by ID with all tracks, using the defined generic classes. Fetch a playlist by ID with all tracks.
""" """
limit = 50 limit = 50
offset = 0 offset = 0
@@ -262,35 +364,8 @@ class SpotifyClient(MusicProviderClient):
offset += limit offset += limit
# Use utility methods to parse tracks playlist_data["content"]["items"] = all_items
tracks = [self._parse_track(item["itemV2"]["data"]) for item in all_items] return self._parse_playlist(playlist_data)
images = self._parse_images(playlist_data.get("images", {}).get("items", []))
owner_data = playlist_data.get("ownerV2", {}).get("data", {})
owner = self._parse_owner(owner_data)
return Playlist(
id=playlist_id,
name=playlist_data.get("name", ""),
uri=playlist_data.get("uri", ""),
external_urls=self._parse_external_urls(playlist_id, "playlist"),
description=playlist_data.get("description", ""),
public=playlist_data.get("public", None),
collaborative=playlist_data.get("collaborative", None),
followers=playlist_data.get("followers", 0),
images=images,
owner=owner,
tracks=[
PlaylistTrack(
added_at=item.get("addedAt", {}).get("isoString", ""),
added_by=None,
is_local=False,
track=track
)
for item, track in zip(all_items, tracks)
]
)
def search_tracks(self, query: str, limit: int = 10) -> List[Track]: def search_tracks(self, query: str, limit: int = 10) -> List[Track]:
""" """
@@ -428,3 +503,83 @@ class SpotifyClient(MusicProviderClient):
except Exception as e: except Exception as e:
print(f"An error occurred while fetching account attributes: {e}") print(f"An error occurred while fetching account attributes: {e}")
return None return None
def browse_all(self, page_limit: int = 50, section_limit: int = 99) -> List[BrowseSection]:
"""
Fetch all browse sections with cards.
:param page_limit: Maximum number of pages to fetch.
:param section_limit: Maximum number of sections per page.
:return: A list of BrowseSection objects.
"""
query_parameters = {
"operationName": "browseAll",
"variables": json.dumps({
"pagePagination": {"offset": 0, "limit": page_limit},
"sectionPagination": {"offset": 0, "limit": section_limit}
}),
"extensions": json.dumps({
"persistedQuery": {
"version": 1,
"sha256Hash": "cd6fcd0ce9d1849477645646601a6d444597013355467e24066dad2c1dc9b740"
}
})
}
encoded_query = urlencode(query_parameters)
url = f"pathfinder/v1/query?{encoded_query}"
try:
response = self._make_request(url)
browse_data = response.get("data", {}).get("browseStart", {}).get("sections", {})
sections = browse_data.get("items", [])
return [self._parse_browse_section(section) for section in sections]
except Exception as e:
print(f"An error occurred while fetching browse sections: {e}")
return []
def browse_page(self, card: BrowseCard) -> List[Playlist]:
"""
Fetch the content of a browse page using the URI from a BrowseCard.
:param card: A BrowseCard instance with a URI starting with 'spotify:page'.
:return: A list of Playlist objects from the browse page.
"""
if not card.uri.startswith("spotify:page"):
raise ValueError("The BrowseCard URI must start with 'spotify:page'.")
query_parameters = {
"operationName": "browsePage",
"variables": json.dumps({
"pagePagination": {"offset": 0, "limit": 10},
"sectionPagination": {"offset": 0, "limit": 10},
"uri": card.uri
}),
"extensions": json.dumps({
"persistedQuery": {
"version": 1,
"sha256Hash": "d8346883162a16a62a5b69e73e70c66a68c27b14265091cd9e1517f48334bbb3"
}
})
}
encoded_query = urlencode(query_parameters)
url = f"pathfinder/v1/query?{encoded_query}"
try:
response = self._make_request(url)
browse_data = response.get("data", {}).get("browse", {})
sections = browse_data.get("sections", {}).get("items", [])
playlists = []
for section in sections:
section_items = section.get("sectionItems", {}).get("items", [])
for item in section_items:
content = item.get("content", {}).get("data", {})
if content.get("__typename") == "Playlist":
playlists.append(self._parse_playlist(content))
return playlists
except Exception as e:
print(f"An error occurred while fetching the browse page: {e}")
return []