Source code for abc_radio_wrapper.abc_radio_wrapper

"""
Main and only module used to search through the abcradio API.

ABCRadio class is used to conduct the search. The other classes
are used to provide structured data and functionality to the result

"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Iterator, List, Optional, TypedDict, cast

import requests
from typing_extensions import Unpack

BASE_URL = "https://music.abcradio.net.au/api/v1/plays/search.json"


[docs]class ABCRadio: """ API wrapper for accessing playlist history of various Australian Broadcasting Corporation radio channels """ def __init__(self) -> None: """ Initialize the ABCRadio class for searching """ self.available_stations: List[ str ] = "jazz,dig,doublej,unearthed,country,triplej,classic,kidslisten".split(",") self.BASE_URL: str = BASE_URL self.latest_search_parameters: Optional[RequestParams] = None
[docs] def search(self, **params: Unpack[RequestParams]) -> "SearchResult": """Send request to abc radio API endpoint and create SearchResult instance Parameters ---------- **params: RequestParams params["channel"]:str any of channels in self.available_stations params['startDate']: datetime """ query_url = self.BASE_URL + self.construct_query_string(**params) r = requests.get(query_url) json_respose = r.json() result = SearchResult.from_json(json_input=json_respose) self.latest_offset = result.offset self.latest_search_parameters = cast(RequestParams, params) return result
[docs] @staticmethod def construct_query_string(**params: Unpack[RequestParams]) -> str: """ Construct query string to communicate with ABC radio API Returns _______ str e.g."?from=2020-04-30T03:00:00.000000Z&station=triplej&offset=0&limit=10" internally this will return the keys order:'from','to','station',''offset','limit' although the ordering is not a requirement of the underlying web API """ from_ = params.pop("from_", None) to = params.pop("to", None) station = params.pop("station", None) offset = params.pop("offset", None) limit = params.pop("limit", None) params_list = [] if from_ is not None: params_list.append("from=" + from_.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) if to is not None: params_list.append("to=" + to.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) if station is not None: params_list.append("station=" + str(station)) if offset is not None: params_list.append("offset=" + str(offset)) if limit is not None: params_list.append("limit=" + str(limit)) if len(params_list) >= 1: return "?" + "&".join(params_list) else: return ""
[docs]class RequestParams(TypedDict, total=False): """ **kwarg arguments to be used when searching in the ABC web api Parameters ---------- from_: datetime The earliest data starts from "2014-04-30T03:00:04+00:00" to: datetime to value should be greater than from_ limit: int number of results to display in a single request, effective limit is 100 offset: int index at which to start returning results, you can iterate through all radio plays by updating this value. See continuous_search for an example implementation. station: str any one of: "jazz,dig,doublej,unearthed,country,triplej,classic,kidslisten" """ from_: datetime to: datetime limit: int offset: int station: str
[docs]@dataclass class RadioSong: """ Dataclass for each entity returned from ABCradio.search, A RadioSong is a Song played at a specific time on a specific channel. Attributes ---------- played_time: datetime original playtime, including timezone information channel: str Name of channel in which song was played e.g. "triplej","doublej","classic","jazz","unearthed","country" song: Song contains metadata of the song including artist and album details """ played_time: datetime channel: str song: Song
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> RadioSong: """ Create RadioSong instance based on json_input Parameters ---------- json_input : dict[str,Any] In the format of: {"entity":"Play", "arid":"...", "played_time":"2020-01-01T12:00:00+00:00" "service_id":"triplej" "recording":..., # Song details (see Song class for more details) "release": ... # Album details (see Album class for more details) } Returns _______ RadioSong """ song = Song.from_json(json_input) return cls( played_time=datetime.fromisoformat(json_input["played_time"]), channel=json_input["service_id"], song=song, )
[docs]@dataclass class SearchResult: """ Dataclass returned from ABCRadio.search """ total: int offset: int limit: int radio_songs: List["RadioSong"]
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> SearchResult: """ Create hierarchy of objects using the result of a single request. To see the expected json_format: https://music.abcradio.net.au/api/v1/plays/search.json """ radio_songs = [] for radio_song in json_input["items"]: radio_songs.append(RadioSong.from_json(radio_song)) return cls( total=json_input["total"], offset=json_input["offset"], limit=json_input["limit"], radio_songs=radio_songs, )
[docs]@dataclass class Song: """ Dataclass to represent a song """ title: str duration: int artists: List["Artist"] album: Optional[Album] url: Optional[str]
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> Song: """ Create Song instance based on json_input Parameters ---------- json_input : dict[str,Any] In the format of: {"entity":"Play", "arid":"...", "played_time":"2020-01-01T12:00:00+00:00", "service_id":"triplej", "recording":..., # Song details "release": ... # Album details } Returns _______ Song """ try: # Occassionally release information is not present or only exists # under recordings.releases json key json_release = ( json_input["release"] if json_input["release"] else json_input["recording"]["releases"][0] ) album = Album.from_json(json_release) artists = [] for artist in json_release["artists"]: artists.append(Artist.from_json(artist)) except (KeyError, IndexError, TypeError): artists = [] album = None url = Song.get_url(json_input) return cls( title=json_input["recording"]["title"], duration=json_input["recording"]["duration"], artists=artists, album=album, url=url, )
[docs] @staticmethod def get_url(json_input): """ Occassionally the url to musicbrainz will be missing, make the proper check and return the url if it exists otherwise return null """ if len(json_input["recording"]["links"]) >= 1: return json_input["recording"]["links"][0]["url"] else: return None
[docs]@dataclass class Artist: """ Dataclass to represent Artists Attributes ---------- url: Optional[str] url that points to musicbrainz info page for the artist name: str Name of the artist e.g. "Justin Bieber" is_australian: Optional[bool] Almost always will be null, the underlying REST api rarely provides a value """ url: Optional[ str ] # http://musicbrainz.org/ws/2/artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da\?inc\=aliases name: str is_australian: Optional[bool]
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> Artist: """ Construct the Artist object from the json representation in https://music.abcradio.net.au/api/v1/plays/search.json """ if json_input["is_australian"] is None: is_australian = None else: is_australian = bool(json_input["is_australian"]) if len(json_input["links"]) >= 1: url = json_input["links"][0]["url"] else: url = None return cls(url=url, name=json_input["name"], is_australian=is_australian)
[docs]@dataclass class Album: """ Dataclass to represent an album (referred to as "releases" in underlying web API). A song can be featured on several albums. """ url: Optional[str] title: str artwork: Optional[Artwork] release_year: Optional[int]
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> Album: try: artwork = Artwork.from_json(json_input["artwork"][0]) except IndexError: artwork = None return cls( url=Album.get_url(json_input), title=json_input["title"], release_year=int(json_input["release_year"]) if json_input["release_year"] else None, artwork=artwork, )
[docs] @staticmethod def get_url(json_input): if len(json_input["links"]) >= 1: return json_input["links"][0]["url"] else: return None
[docs]@dataclass class Artwork: """ Dataclass to represent the artwork of an associated Album. Each album can have several artworks and each artwork can have several image formats/sizes. """ url: str type: str sizes: List[ArtworkSize]
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> Artwork: sizes: List[ArtworkSize] = [] for size in json_input["sizes"]: sizes.append(ArtworkSize.from_json(size)) return Artwork(url=json_input["url"], type=json_input["type"], sizes=sizes)
[docs]@dataclass class ArtworkSize: """ Dataclass to represent the image format/size for each artwork. Most typical use case is providing large images or thumbnails for each different interface. """ url: str width: int height: int aspect_ratio: str
[docs] @classmethod def from_json(cls, json_input: dict[str, Any]) -> ArtworkSize: return cls( url=json_input["url"], width=json_input["width"], height=json_input["height"], aspect_ratio=json_input["aspect_ratio"], )
@property def aspect_ratio_float(self) -> float: width_ratio, height_ratio = self.aspect_ratio.split("x") return int(width_ratio) / int(height_ratio)