diff --git a/examples/find_albums.py b/examples/albums.py similarity index 69% rename from examples/find_albums.py rename to examples/albums.py index 60258fb..2594e4f 100755 --- a/examples/find_albums.py +++ b/examples/albums.py @@ -1,6 +1,4 @@ -""" -Find an album using the `spotify_actions` API and echo its contents. -""" +# pylint: disable=missing-module-docstring, missing-function-docstring from argparse import ArgumentParser @@ -9,9 +7,9 @@ from spotify_actions.search import search_albums from spotify_actions.util import read_credentials -def main() -> None: # pylint: disable=missing-function-docstring +def main() -> None: parser = ArgumentParser() - parser.add_argument("-f", "--config", required=True) + parser.add_argument("-c", "--credentials", required=True) parser.add_argument("search_string") cmdline = parser.parse_args() diff --git a/examples/label_recent_releases.py b/examples/label_recent_releases.py new file mode 100644 index 0000000..6b226cb --- /dev/null +++ b/examples/label_recent_releases.py @@ -0,0 +1,32 @@ +# pylint: disable=missing-module-docstring, missing-function-docstring + +from argparse import ArgumentParser +from datetime import date, timedelta + +from spotify_actions.echo import echo_album +from spotify_actions.join import join_albums +from spotify_actions.search import search_albums_label +from spotify_actions.temporal import album_released_after +from spotify_actions.util import read_credentials + + +def main() -> None: + parser = ArgumentParser() + parser.add_argument("-c", "--credentials", required=True) + parser.add_argument("label", nargs="+") + + cmdline = parser.parse_args() + + today = date.today() + four_weeks = timedelta(days=28) + + client = read_credentials(cmdline.credentials) + + label_albums = [search_albums_label(client, l) for l in cmdline.label] + albums = join_albums(*label_albums) + albums_recent = album_released_after(albums, today - four_weeks) + echo_album(albums_recent) + + +if __name__ == "__main__": + main() diff --git a/spotify_actions/join.py b/spotify_actions/join.py new file mode 100644 index 0000000..2dd4e94 --- /dev/null +++ b/spotify_actions/join.py @@ -0,0 +1,14 @@ +""" +Join objects from multiple sources into a single stream +""" +from typing import Iterable + +from spotify_model import SearchAlbum + + +def join_albums(*args: Iterable[SearchAlbum]) -> Iterable[SearchAlbum]: + "Join the results of many album producers by exhausting all albums from each producer" + + for arg in args: + for album in arg: + yield album diff --git a/spotify_actions/search.py b/spotify_actions/search.py index de94b62..10aa45e 100644 --- a/spotify_actions/search.py +++ b/spotify_actions/search.py @@ -23,8 +23,15 @@ def search_albums(client: Spotify, search_str: str) -> Iterable[SearchAlbum]: yield SearchAlbum(**item) -def search_label_albums(client: Spotify, search_str: str) -> Iterable[SearchAlbum]: +def search_albums_artist(client: Spotify, artist_name: str) -> Iterable[SearchAlbum]: + "Display albums from a particular artist" + + for item in exhaust(_search_albums(client, f'artist:"{artist_name}"')): + yield SearchAlbum(**item) + + +def search_albums_label(client: Spotify, label_name: str) -> Iterable[SearchAlbum]: "Display albums from a particular label" - for item in exhaust(_search_albums(client, f'label:"{search_str}"')): + for item in exhaust(_search_albums(client, f'label:"{label_name}"')): yield SearchAlbum(**item) diff --git a/spotify_actions/temporal.py b/spotify_actions/temporal.py new file mode 100644 index 0000000..548d13b --- /dev/null +++ b/spotify_actions/temporal.py @@ -0,0 +1,49 @@ +""" +Actions for filtering based on temporal information +""" + +from calendar import monthrange +from datetime import date, datetime +from logging import getLogger +from typing import Iterable + +from spotify_model import ReleaseDatePrecision, SearchAlbum + + +def album_released_after(albums: Iterable[SearchAlbum], released_after: date) -> Iterable[SearchAlbum]: + """ + Filter albums to after a specific release date. + + For albums that don't have date-level precision, the release date is treated as the final day within that period; + thus, albums released in "1981" are effectively released on 1981-12-31, and albums released in "1981-07" are + treated as "1981-07-31" + """ + + logger = getLogger(__name__) + + for album in albums: + if album.release_date_precision == ReleaseDatePrecision.YEAR: + actual_release = datetime.strptime(album.release_date, "%Y") + effective_release = date(actual_release.year, 12, 31) + elif album.release_date_precision == ReleaseDatePrecision.MONTH: + actual_release = datetime.strptime(album.release_date, "%Y-%m") + final_day = monthrange(actual_release.year, actual_release.month)[1] - 1 + effective_release = date(actual_release.year, actual_release.month, final_day) + else: + effective_release = datetime.strptime(album.release_date, "%Y-%m-%d").date() + + if effective_release >= released_after: + logger.debug( + "Including album=%s released on date=%s (prior to date=%s)", + album.name, + effective_release, + released_after, + ) + yield album + else: + logger.debug( + "Skipping album=%s released on date=%s (prior to date=%s)", + album.name, + effective_release, + released_after, + )