Skip to content

Conversation

@YeeP79
Copy link

@YeeP79 YeeP79 commented Dec 14, 2025

Adds support for browsing Spotify's new releases via spotify:playlists:new-releases.

Closes #241

Changes

  • Added "New releases" directory under spotify:playlists
  • Uses the browse/new-releases Spotify API endpoint
  • Returns album refs like the existing "Featured" playlists

Testing

All existing tests pass, plus added tests for:

  • Happy path with paginated results
  • Empty results
  • Offline/not logged in
  • Unknown variant handling
  • Edge cases (malformed API responses)

Screenshots

Iris UI:
new_releases

API Response:

curl -X POST http://localhost:6680/mopidy/rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"core.library.browse","params":{"uri":"spotify:playlists:new-releases"}}'
{
  "result": [
    {"name": "Taylor Swift - THE TORTURED POETS DEPARTMENT", "type": "album", "uri": "spotify:album:1Mo4aZ8pdj6L1jx8zSwJnt"},
    {"name": "Pearl Jam - Dark Matter", "type": "album", "uri": "spotify:album:7MNrrItJpom6uMJWdT0XD8"},
    {"name": "Lucky Daye - HERicane", "type": "album", "uri": "spotify:album:4YQ8O3PQb7cZnnLeqNPaa1"},
    ...
  ]
}

Note to Maintainers

I considered making _browse_playlists more SRP compliant and to mirror the structure of _browse_album() and _browse_artist() from the get go, but I did not want to complicate this PR. Happy to submit another that takes care of that after this is merged, or include it in this. Let me know.

Adds 'New releases' to the browse playlists directory, allowing users
to browse Spotify's new album releases via spotify:playlists:new-releases.

- Add 'New releases' entry to _PLAYLISTS_DIR_CONTENTS
- Extend _browse_playlists() to handle 'new-releases' variant
- Add Google-style docstring documenting the function
- Add logging for unknown playlist variants
- Add comprehensive tests with API documentation references

Closes mopidy#241
Copy link
Member

@jodal jodal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking the time to add this feature!

Comment on lines +187 to +196
"""Browse playlist-related directories (featured playlists, new releases).
Args:
web_client: The Spotify OAuth client for API requests.
variant: The playlist variant to browse ('featured' or 'new-releases').
Returns:
A list of Mopidy Ref objects (playlists for 'featured', albums for
'new-releases').
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is better encoded as type hints:

def _browse_playlists(
    web_client: SomeTypeIDidntLookupNow,
    variant: Literal["featured", "new-releases",
) -> list[Ref]:

Comment on lines +201 to +210
items = flatten(
[
page.get("playlists", {}).get("items", [])
for page in web_client.get_all(
"browse/featured-playlists",
params={"limit": 50},
)
if page
]
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that you're following the pattern of existing code, but I'm leaving a comment anyway:

I think you can drop the call to flatten() if you just change page.get(...) to *page.get(...). If you do the same with the other calls to flatten() in this file, we can get rid of the util function.


if variant != "featured":
return []
if variant == "featured":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the function is typed with variant as a Literal, that makes the if variant = ... statements a prime candidate for a match variant: instead. We're now requiring a new enough Python version for match to be used freely 🎉

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Thank you for the explanation, I'm still learning Python patterns and the API.



def test_browse_playlists_directory(provider):
"""Test browsing the playlists directory returns all playlist categories."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, skip comments if they don't add anything, e.g. important reasoning for why something is the way it is.



def test_browse_new_releases(web_client_mock, web_album_mock_base, provider):
"""Test browsing new releases returns album refs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most of these comments can be removed without loosing anything. Superflous comments adds content that gets outdated if not maintained, so they are not free.

assert "Unknown URI type" in caplog.text


# Defensive programming tests - verifying robust handling of edge cases
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this set of tests takes things a bit far. Having one parametrized test that tests a few of these cases is okay, but we don't need to spend 80 lines of code on testing every possible permutation of broken responses.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I say "parametrized test", I'm referring to this Pytest feature: https://docs.pytest.org/en/stable/how-to/parametrize.html#parametrize-basics

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this set of tests takes things a bit far. Having one parametrized test that tests a few of these cases is okay, but we don't need to spend 80 lines of code on testing every possible permutation of broken responses.

Thanks for the feedback! I appreciate you taking the time to review this thoroughly.
I hear you on the test coverage. Coming from a Node background, I tend toward comprehensive test cases as documentation and regression protection, but I recognize that may not align with this project's patterns. I'll convert these to a parametrized test as you've suggested - it's a good opportunity for me to learn the preferred testing approach here.
Thanks again for the guidance.

@YeeP79
Copy link
Author

YeeP79 commented Dec 21, 2025

If I might be so bold: while I'm doing this would you mind if I take a stab at converting _browse_playlists to more of an SRP compliant method?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support "New Releases"

2 participants