Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spotify provider #22

Merged
merged 22 commits into from
Jun 10, 2024
Merged

Spotify provider #22

merged 22 commits into from
Jun 10, 2024

Conversation

phw
Copy link
Collaborator

@phw phw commented Jun 8, 2024

This implements a Spotify provider (closes #16).

Similar to Tidal this requires API credentials, which can be setup on https://developer.spotify.com/dashboard/ . The API key and secret must be provided by the environment variables HARMONY_SPOTIFY_CLIENT_ID and HARMONY_SPOTIFY_CLIENT_SECRET.

I addressed all the points I brought up in #16. About the linked track thing see my comment #16 (comment)

Loading a release with more than 50 tracks requires additional requests. This is a large one with 100 tracks across 7 media: https://open.spotify.com/intl-de/album/2ey9jImi467qEu67fvW1kP

The copyright info is returned differently:

The provider normalizes all cases to start with the © or ℗ symbol, depending on the type.

A few things we might consider for future refinement, but which I think should be put into separate PRs:

  • For now the available regions are hardcoded as they are for other providers. But for Spotify we could load them dynamically via the https://developer.spotify.com/documentation/web-api/reference/get-available-markets endpoint. But this probably requires some more extensive refactoring.
  • The authentication with OAuth2 client credentials flow is very similar between Tidal and Spotify. I partially applied some refactoring to have at least the caching of the access token centralized, so not every implementation invents its own solution. But I think there could be more generalization.

@kellnerd kellnerd added feature New feature or request provider Metadata provider labels Jun 9, 2024
@phw
Copy link
Collaborator Author

phw commented Jun 9, 2024

We can factor out the copyright normalization logic and reuse it for other providers, e.g. as suggested for Tidal in https://community.metabrainz.org/t/harmony-music-metadata-aggregator-and-musicbrainz-importer/698641/15

But I'd do this after merging this PR. It also needs some further research. I know Tidal includes the copyright text both with and without the © symbol. What I'm unsure is whether this strictly contains copyright © info, or whether it also can sometimes contain phonographic copyright ℗ info. Spotify has those separated, which makes it easier.

Copy link
Owner

@kellnerd kellnerd left a comment

Choose a reason for hiding this comment

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

Thank you very much for leading the development of new providers!
Code looks good, I will do some testing later today and I have a few questions as usual.

});

readonly features: FeatureQualityMap = {
'cover size': 640,
Copy link
Owner

Choose a reason for hiding this comment

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

Regarding the maximum resolution, there are good news: qsniyg/maxurl#1329
But I guess we will keep the 640 pixels here until IMU officially supports larger resolutions.

providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/base.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
@phw phw requested a review from kellnerd June 10, 2024 06:06
Copy link
Owner

@kellnerd kellnerd left a comment

Choose a reason for hiding this comment

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

I have done a bit more testing, especially related to the barcodes.
Unfortunately Spotify's API always seems to do a string comparison for these, so we won't get away with always padding the GTIN to 14 digits before doing a (first and only) search. Maybe we even have to add the 13 digit case back, we will see. In any case padding to 13 digits should be the last attempt as it is the rarest case.

I also wanted to note that the provider is currently not rate limited, probably because the Spotify docs don't specify a specific quota, only the length (30s) of the rate limit interval.
We should probably handle 429 errors and make use their header to implement a retry strategy, but this is rather a general feature which other providers also could make use of.

providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Show resolved Hide resolved
providers/base.ts Outdated Show resolved Hide resolved
utils/image.ts Outdated Show resolved Hide resolved
utils/label.test.ts Outdated Show resolved Hide resolved
utils/label.test.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
providers/Spotify/mod.ts Outdated Show resolved Hide resolved
phw added 2 commits June 10, 2024 14:32
This ensures GTIN lookups give results for barcodes not available in the
default region. ID lookups must be performed without region, only then
the API will return available_markets.
Also try with GTIN padded to 13 characters again, but only as a last resort.
@phw
Copy link
Collaborator Author

phw commented Jun 10, 2024

Maybe we even have to add the 13 digit case back, we will see. In any case padding to 13 digits should be the last attempt as it is the rarest case.

I added it back in 75fe532, but refactored the code to be more readable and also try 13 character padding last. This should handle the eventual case of 13 characters without complicating the code or requiring unnecessary requests.

@phw phw requested a review from kellnerd June 10, 2024 14:25
Copy link
Owner

@kellnerd kellnerd left a comment

Choose a reason for hiding this comment

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

That was a long review phase, but I am very happy with the outcome, especially that we have solved one longstanding issue of a-tisket. One last issue and this is good to go.

I think the current strategy to iterate over regions before GTIN barcodes makes perfect sense. We probably still do less requests than a-tisket on average and in the long run we could limit the number of fallback regions which are considered.
For now the users can specify as many as they want, but they will be filtered by available regions of releases whose providers have already been looked up by URL:

harmony/lookup.ts

Lines 246 to 259 in 85414bd

// Remove all preferred regions where the release is unlikely to be available.
if (this.options.regions?.size) {
const availablePreferredRegions = new Set(this.options.regions);
for (const region of availablePreferredRegions) {
if (!availableRegions.has(region)) {
availablePreferredRegions.delete(region);
}
}
this.options.regions = availablePreferredRegions;
}
// Use available regions if no (available) preferred regions remain.
if (!this.options.regions?.size) {
this.options.regions = availableRegions;
}

Comment on lines 120 to 126
if (this.lookup.region) {
query.set('market', this.lookup.region);
}
if (this.lookup.method === 'gtin') {
lookupUrl = new URL(`search`, this.provider.apiBaseUrl);
query.set('type', 'album');
query.set('q', `upc:${this.lookup.value}`);
Copy link
Owner

Choose a reason for hiding this comment

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

The market should only be set for GTIN lookups, otherwise Harmony will use the region which it extracted from the intl-xx part of the Spotify URL for URL/ID lookups:

Suggested change
if (this.lookup.region) {
query.set('market', this.lookup.region);
}
if (this.lookup.method === 'gtin') {
lookupUrl = new URL(`search`, this.provider.apiBaseUrl);
query.set('type', 'album');
query.set('q', `upc:${this.lookup.value}`);
if (this.lookup.method === 'gtin') {
lookupUrl = new URL(`search`, this.provider.apiBaseUrl);
query.set('type', 'album');
query.set('q', `upc:${this.lookup.value}`);
if (this.lookup.region) {
query.set('market', this.lookup.region);
}

With this change it is also no longer necessary to reset this.lookup.region to undefined after the initial GTIN search.

P.S. Is the intl-xx part actually a country code or a language code?
I made this mistake for the Deezer provider where I only noticed it when iTunes requests for the region EN were failing 😂
For Spotify it is a bit unclear, MBS-13052 assumes it is a language, but for all cases which I have tried where language code and country code would be different, Spotify always seems to redirect to the URL without the intl part (en vs us or gb, sv vs se), so I can't tell for sure!?
If it is a language, the URLPattern should not use :region but :language or something else, but as it stands it is currently not necessary to change something because all intl codes which I have seen so far are valid region codes.

Copy link
Owner

@kellnerd kellnerd Jun 10, 2024

Choose a reason for hiding this comment

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

After doing a few more tests (documented in the comments of the MBS ticket) and despite lots of indications making me think the opposite, I am now confident this is a language code (intl-ar is for Arabic and not for Argentina).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See 72bbf23

Regarding the code: I honestly don't know. I always interpreted it as the market code. But actually it is hard to test. It nearly always redirects to as you say. The redirection seems to be controlled by the Accept-Language header, so that's an argument it is a language and not a market. Do you know any other intl-* paths being used?

curl -v -H "Accept-Language: fi" https://open.spotify.com/intl-de/album/2Usbb1Ao4tpzDY0IbIQHqB

< HTTP/2 302 
< location: /album/2Usbb1Ao4tpzDY0IbIQHqB
curl -v -H "Accept-Language: de" https://open.spotify.com/album/2Usbb1Ao4tpzDY0IbIQHqB

< HTTP/2 302 
< location: /intl-de/album/2Usbb1Ao4tpzDY0IbIQHqB

For curl -v -H "Accept-Language: de" https://open.spotify.com/intl-de/album/2Usbb1Ao4tpzDY0IbIQHqB there is the no redirect.

I tested the above with a server located in Finland, just to be sure it behaves the same independent of German location.

I changed it to be :language for now. As you say it shouldn't matter, but at least it avoids any issue if this being the region turns out to be wrong.

@kellnerd kellnerd merged commit 4f583c9 into kellnerd:main Jun 10, 2024
2 checks passed
@kellnerd
Copy link
Owner

I have also tested the behavior if no credentials are specified and did a similar improvement for the Tidal provider to show the underlying error instead of the generic one:
image

@phw phw deleted the spotify branch June 10, 2024 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request provider Metadata provider
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Spotify provider
2 participants