Skip to content

Conversation

feathecutie
Copy link

@feathecutie feathecutie commented Jun 25, 2025

(Mostly) fixes #31

TODO

Original message

This PR provides an initial draft of a YouTube Music provider for harmony. I intend to continue working on this, but I wanted to try to get some eyes on this already, both for potentially some feedback and help with things I'm unsure about, and also as a general motivator for me to keep working on this.

What works well (enough) already:

  • Resolving GTINs to YouTube Music releases
  • Looking up the URLs of YouTube Music releases and extracting the most essential info from them (but sadly no GTIN, as I don't think YouTube exposes this anywhere :/)
  • Extract tracks from releases and provide their most essential info
  • Extract artists (at least for the release, not for individual tracks yet)
  • Amazingly enough, the data we're looking up here does not even need any authentication/token/cookies (at least it works well in my testing without any, but that might be a fluke since I was using cookies until very recently. In case the requests do fail while testing, see this section in the YouTube.js guide for how to get authentication cookies)

Things I'm not yet happy with / still unsure about:

  • As mentioned before, there seems to be no way to extract the GTIN from YouTube Music releases. This means that YouTube Music is not a great initial data source for a release that has no other providers yet, especially since YouTube does not expose nearly as much metadata as other providers do. If anyone does know of a way to get GTINs from YouTube Music albums, please let me know^^
  • This draft currently uses the YouTube.js library in order to communicate with YouTube's internal API. I've noticed that harmony currently has no other provider-specific dependencies, and I'd understand if it isn't ideal to pull in such an extensive dependency for a single provider.
    One could in theory try to reimplement the parts of YouTube.js we are using, but the internal API that YouTube.js is using is hardly documented and most likely very sensitive to slightly incorrect requests that YouTube.js already correctly handles, not to mention that keeping up with API changes seems unnecessarily hard.
  • As mentioned in the previous point, YouTube.js uses the internal YouTube API, also known as "InnerTube". This is sadly necessary as YouTube music provides no consumer-facing API on it's own, and the normal YouTube API is not powerful enough for the data we're looking up. While I haven't encountered any issues and haven't heard of legal consequences regarding this, I'm definitley no lawyer, so this might potentially be questionable from a legal standpoint. I'd love some feedback on whether the usage of this kind of API is okay in harmony, because I really don't think a YouTube Music provider is possible without it :(
  • I'm not familiar with how often parts of the internal YouTube API actually change, but this provider might definitely be a bit higher maintenance than other providers, although YouTube.js does luckily take care of all the messy internals regarding this. I'm also happy to maintain this provider whenever things do break, but I can't promise I'll always be available or will notice such things right away
  • And as a final pain point.. my code for fetching data is just really messy at some points, and requires more API calls than I'd like. Especially the part for converting between playlists and albums (starting at line 122 in YoutubeMusic/mod.ts) currently has to use some weird workarounds, but I'm still trying to find a better and more direct way to get the data we need here, and for now, while less elegant it still works

This is also still missing tests, I'll try to add some soon

..also, I really hope this paragraph doesn't sound too LLM-generated, this definitely came out a lot more "formal" than I planned^^

@feathecutie feathecutie force-pushed the youtube-music-provider branch 3 times, most recently from ec8cbc6 to a50561b Compare June 25, 2025 23:51
@kellnerd kellnerd added feature New feature or request provider Metadata provider labels Jun 26, 2025
@kellnerd kellnerd self-requested a review June 26, 2025 06:20
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 for tackling this! Before I get into any implementation details, I wanted to share a few general thoughts.

There were two things I was worried about regarding the YouTube.js library:

  1. Code bloat:
    • I ran deno info lookup.ts | head and temporarily commented out the YouTubeMusicProvider import in providers/mod.ts to get a feeling for the impact.
    • without YouTube Music provider: 2.12MB, 225 unique dependencies
    • with YouTube Music provider: 4.13MB, 982 unique dependencies
    • This looks like a lot, but if I include the whole web app with deno info server/main.ts | head, it shouldn't matter too much: 14.92MB, 1398 unique dependencies (YouTube included).
    • I've also tried to replicate the relevant requests by hand with fetch and failed to make them work, the API expects a couple of headers and a ton of cruft inside the POST body to even consider the request. So we should really use the library and spare us the extra hassle.
  2. Permalinks and caching won't work: Lookups for all other providers are cached and used for 24 hours, older snapshots can be accessed using permalinks which contain a timestamp.
    • We can't use the usual provider caching mechanism (SnapStorage) because we don't have control over individual requests when we use the library.
    • While it is possible to pass a custom fetch function to Innertube, we can't proxy the requests to fetchSnapshot/fetchJSON (which use Harmony's cache) because we would have to pass the timestamp well.
    • Additionally, as opposed to any proper public API, the URL is always https://www.youtube.com/youtubei/v1/browse?prettyPrint=false&alt=json for every lookup, because the relevant lookup parameters are only contained in the body of the POST request. This is not currently supported by Harmony's cache which uses the URL as key.
    • Even if the cache would support distinguishing POST requests by body data and if we hacked Innertube to pass the timestamp along, the API response contains a lot of unnecessary clutter (e.g. describing the player UI), so I wouldn't want to cache the whole thing anyway.
    • I guess our best way forward is to omit YouTube caching for now or to implement separate caching logic which caches the results of the Innertube calls. If you want to rather focus on the lookup implementation, I can help you with this task.

P.S. I know that there are still some open questions and I guess I also have more feedback, but this should be enough to get you started (and it's getting too hot to think here 😅).

P.P.S. One last thing: Please follow https://www.conventionalcommits.org/en/v1.0.0/ (see the existing commits, the scope would be "YouTube" for all follow-ups). Eventually I will use this to generate changelogs for the website 😇

@rinsuki
Copy link

rinsuki commented Jul 9, 2025

As a hobby YTM (=YouTube Music) internals researcher, I wouldn't recommend you to automatically scrapes YTM, because...

  • current code treats every tracks are free streaming, but actually some tracks requires (YouTube/YTM) Premium subscription which should be mapped as a (not free) "streaming page" in MB.
    • you might be think "then map unavailable track to paid streaming track"... but its not actually true:
    • there are actual "unavailable" track (can't be played even with premium subscription, like some "purchase only" tracks in Apple Music), but both are marked as "unavailable" if you visiting as free/anonymous user.
    • which means, If you wanted to detect free/paid streaming correctly, needs premium account's session.
    • well I think "scrapes using premium account session" is bad idea.
  • If you are visiting music.youtube.com/playlist (album page, incl. InnerTube) as free/anonymous user, some tracks are silently replaced with "Music Video" (actual MV, not the "Provided to YouTube by ...." one).
    • this happens even "Provided to YouTube by ..." video is available for free user.
    • workaround: visit www.youtube.com/playlist page, they wouldn't replace tracks with Music Video.
    • but there are new bad news: some albums are broken and wouldn't returns any (including free one) tracks in www.youtube.com/playlist page.
    • so yet another workaround comes in: use the YouTube Data API (public one) to get playlist content, create new playlist, and check it from InnerTube.
    • but I think automatically creates playlist sounds suspicious and I think your account might be banned if you do that.
  • Harmony have a region selector, but by obvious reasons, InnerTube always uses your IP address to decide region, so region selector will not work.
    • which means some album (e.g. JP exclusive release) will not able to seed.
    • even worse, some albums have a different Playlist/Track ID, based on your region.
  • as you know, search "barcode" in YTM would returns album which tied to that barcode, yeah? sometimes not.
    • If album have a multiple versions (e.g. early release, different artwork (pretty common in anime songs), different B-side track), YTM sometimes return "other versions" album in the search result.
    • You can see original album in their albums page's "other versions" section, but... we need to add additional GUI in the Harmony i think.
  • I couldn't remember, but there are some additional weird behaviour which isn't noted here.

@feathecutie feathecutie force-pushed the youtube-music-provider branch from a50561b to ebcf9e4 Compare July 9, 2025 07:15
@feathecutie
Copy link
Author

First of all, thanks a lot for the review @kellnerd!
I applied most of your suggestions, but I definitely still have a lot of work to do here^^
Note that the current commit history of this PR might be kind of weird because I ended up vendoring some dependencies until some issues are fixed, but this is definitely temporary, see the commit message of ebcf9e4 for details

@rinsuki, thanks for your thoughts!
I know that scraping the interal YouTube (Music) API is definitely not gonna be a clean and easy process, but I'm not sure I agree that using that data is a bad idea in general.

I will definitely try to improve my code to make as few assumptions as possible, as some of the ones it's currently making are clearly wrong (e.g. the "free streaming" thing). I get that these things are really hard to be sure about (e.g. to decide whether a track is paid or unavailable), but in cases where I can be sure about the data, I feel like it is still more valuable to return some correct data than none at all.

Something I am pretty unsure about is the region problem, as I don't think there is a good way to work around this besides using proxies (which would probably cost money to use, and are probably already blocked by YouTube anyways). Innertube does provide a location parameter when constructing the instance, with the description "Geolocation setting", but I doubt that that actually does anything useful (haven't tested it yet).

And the barcode thing also seems really messy.. haven't encountered it myself yet but I trust you that it's a thing. This might definitely mean that a YouTube Music provider is a lot less feasible than I thought, but I'd still love to experiment with it for a while before giving up on this^^
Do you happen to have any links/album ids/barcodes/etc lying around where these issues do occur? It's pretty hard to just randomly stumble upon these, and they would be really helpful for testing. If you have any documentation/write-ups for the issues you mentioned, I'd love some references as well because it's kinda hard to find info on this, but I also already really appreciate your comment!

@rinsuki
Copy link

rinsuki commented Jul 9, 2025

There are some examples (NOTE, all of them are checked in Japan IP address (where I live), it might behave differently in another regions):

@feathecutie feathecutie force-pushed the youtube-music-provider branch 4 times, most recently from 6f48f52 to 670eb49 Compare July 13, 2025 23:28
YouTube Music sometimes returns incorrect, alternate versions of a
release when looking up a GTIN.
This warns if a release with multiple versions is returned,
as there is no way of knowing if YouTube returned the correct one.
@feathecutie feathecutie force-pushed the youtube-music-provider branch from 670eb49 to f84c352 Compare July 13, 2025 23:59
@feathecutie
Copy link
Author

Okay, a small (or not so small) update:

The code is (in my opinion) basically feature complete now.

I ended up rewriting this whole thing to not depend on YouTube.js after all, which turned out to be a lot easier than expected (even if the resulting code looks a bit less clean right now).

This code should be fully compatible with the caching and permalink system now (I was able to work around the URL-based cache by simply appending a unique hash to the URLs which gets ignored by YouTube anyways), and all API requests now happen through MetadataProvider.fetchJSON.

Regarding the following concern:

Even if the cache would support distinguishing POST requests by body data and if we hacked Innertube to pass the timestamp along, the API response contains a lot of unnecessary clutter (e.g. describing the player UI), so I wouldn't want to cache the whole thing anyway.

I am currently returning (and thus caching) the whole API response from fetchJSON.
The responses I'm getting seem to be around ~60000 (or more) characters, so I could try to rewrite the code to do some preprocessing using responseMutators in fetchJSON so only the relevant parts of the response get cached, but that might make the code more complicated (unless I just move the whole parsing into the responseMutator, but that also feels like it would defeat the point).

Besides that, I'm generally happy with the code now (though it could maybe do with a bit more clean up).

Sadly there are still some pain points I haven't been able to work around (and likely won't be able to):

  • There is no way to select regions, and the returned response always corresponds to the source IP address of the request.
    Unfortunately, data on YouTube Music seems to be able to differ significantly between regions (but the most basic metadata should probably stay correct).
    The only way I can see to work around this would be to proxy the requests to different IPs.
  • GTIN lookup is slightly flawed, as sometimes YouTube simply returns the wrong album when searching for a GTIN.
    This seems to only happen to albums with have alternate versions linked in YouTube (so YouTube is returning the wrong version of the album).
    I am trying to detect albums with other versions in the code and throw a warning using ReleaseLookup.warnMultipleResults, but I can't guarantee that this will always work and there sadly simply is no way to check which of the alternate versions of an album is the correct one
  • The other concerns in @rinsuki's comment are likely still valid as well.
    Regarding the difference between unavailable, paid or free tracks and the difference between actual tracks and MVs: For now, I'm simply returning every video for a track that I can get. If both the actual track and the MV are available, I'm returning both, and if not, I'm just returning whatever data I have.
    I'm currently marking the YouTube release as "free streaming" if each track has at least one publicly available video (be it a YouTube Music track or the MV), but this once again is something that doesn't necessarily stay true between regions.

@feathecutie feathecutie marked this pull request as ready for review July 14, 2025 14:05
Comment on lines +21 to +22
hl: 'en',
gl: 'US',
Copy link

Choose a reason for hiding this comment

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

I think we might be want to decide those values based of region parameter

Copy link
Author

Choose a reason for hiding this comment

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

Setting the gl parameter based on the region would probably be a good idea (though it doesn't seem to affect YouTube's behaviour), but I'm not sure how to select one region to use if multiple regions are provided.
As far as I can tell, other providers solve this by using queryAllRegions to query each region separately, but this doesn't make sense in this case since the region parameter doesn't affect anything anyways

Copy link

Choose a reason for hiding this comment

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

I didn't know harmony can set multiple regions...

by the way, my main focus is hl parameter, since YTM (sometimes) have a localized tracklists and will be changed by user's language settings. i didn't check it is depending to this parameters, but I think it will.

(sidenote: but I also think change language parameter by region parameter is good or not? i don't know. Apple Music behave like that, but actually some Apple Music region supports multiple language in one regions and shows based of your device's language settings. e.g. JP region have a japanese and english tracklist)

e.g. https://music.youtube.com/playlist?list=OLAK5uy_nXbgxu51sJwZQz69UwVWVvu-j22_IYvA8 this album have a Japanese tracklist and English tracklist, and if your language settings aren't japanese, it should show english tracklist

I think different tracklist for en-US and en-GB are possibly exists, but I don't know about it actually exists or not.

Copy link
Author

@feathecutie feathecutie Jul 14, 2025

Choose a reason for hiding this comment

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

Yup okay, setting hl to ja seems to work in order to get localized track lists.
I could try to set this dynamically, but I'm still not sure which language to use when.
I think I've read that the MusicBrainz policy is that release and recording titles should always be submitted in their original localisation (so, japanese localisation for Japanese releases), but that would require knowing what the "original" localisation is.
The problem with this is that for example, I've noticed that some releases and tracks tend to have Japanese localisations even if they aren't originally Japanese (like this album by the German band Rammstein)

Copy link
Owner

Choose a reason for hiding this comment

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

When the user specifies multiple preferred regions, they will only be used by providers which support the queryAllRegions fallback. But if YouTube doesn't "fail" to return data for a certain unavailable region (in some way), this concept doesn't make sense and the first "supported" region of the preferred regions should be used.
So unless there is a meaningful list of supported regions for YouTube, I would simply use the first region.

Regarding the language / hl parameter, I wouldn't try to derive this from the region as these aren't always one-to-one mappings. If we arrive at the conclusion that the language is a useful parameter (for YouTube and potentially other providers), Harmony should allow the user the set a preferred language as well. We can even extract the language from the release URL for some providers (namely Deezer, Spotify, Beatport, maybe Qobuz in the future), which would override the preference input just like a region from a release URL does (for Apple).

Comment on lines 38 to 39
browserName: 'Edge Chromium',
browserVersion: '109.0.1518.61',
Copy link

Choose a reason for hiding this comment

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

(note: if you replaced user agent, you might be want to also replace those values, or remove it since server don't care in many cases)

Copy link
Author

Choose a reason for hiding this comment

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

It seems like I'm actually able to remove all body values besides clientName and clientVersion and everything still works as expected.
I'll probably do this, as this seems a lot cleaner (the current body was mostly just copied from YouTube.js requests), but I'm a bit afraid that YouTube might get.. suspicious, if the requests don't contain these values?
I have no idea what actually gets detected and logged by YouTube, but I'm afraid that this might cause YouTube to flag and block harmony quicker

Copy link

Choose a reason for hiding this comment

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

we wouldn't able to know about they cares or not :P

well yeah, if you want to reduce chance of getting banned from YT, I think you need to put realistic values, or even scrape HTML (YT or YTM web server is calling API when rendering HTML, and it is included in the returned HTML, for takeover to client JS. at this case you still need to call API manually for some cases, e.g. paging or album search)

Copy link
Owner

@kellnerd kellnerd Jul 15, 2025

Choose a reason for hiding this comment

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

Regarding the copied code, I think you should add a copyright line to all affected files, probably constants.ts and api_types.ts? I am not a lawyer, but I think something like the following should be sufficient for MIT licensed code:

// Adapted from / Taken from / Inspired by https://github.com/LuanRT/YouTube.js/tree/v14.0.0
// Copyright (c) 2021 LuanRT. MIT license.

Not sure about the wording of the first line, you should know better what is the best description.

@feathecutie
Copy link
Author

feathecutie commented Jul 14, 2025

Okay, after a bit more testing, there are two other things which would probably improve the quality of the data.

For one, some tracks provide a more comprehensive overview of the artist credits with the "View song credits" button in their respective kebab menu (the 3 vertical dots in the song list of the web UI).
This view does sometimes provide a more complete list of artists than the artist credits I'm currently parsing, but using it would require make one additional API request per track, which seems quite excessive. Additionally, these artist credits don't provide join phrases (unlike the currently used credits), and I'm not sure how to decide which list to use if there is a mismatch of artist.

Another issue I encountered is that sometimes tracks and releases display a title with featuring artists in the name, even if YouTube has the correct title without the featuring artist.
For example, in this single called "Classical Dragon", both the release title and the track name contain "(feat. other artist)", but when opening the same playlist in www.youtube.com, the release and track title both correctly show up as simply "Classical Dragon".
I guess I'll be experimenting with this a bit more, as it would be really nice to have the correct names here

@feathecutie feathecutie changed the title Youtube Music provider YouTube Music provider Jul 14, 2025
@kellnerd
Copy link
Owner

kellnerd commented Jul 15, 2025

Great work @feathecutie! Looking good at first glance and the tests are passing as well.
It may take me a while to fully review it, because the next two three weeks are very busy for me.
But it's great to see how much discussion is already happening here, thank you for sharing your thoughts @rinsuki!

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.

Sorry that it took me so long to get back to review this PR, I had significantly less time available than expected this summer.
I agree with your summarized TODO items in the updated PR description and only have a few remarks to add (from my weeks-old notes).

Ideally we should avoid fetching the HTML player just to extract the corresponding browse ID for a playlist.
I still have to compare the two data representations side by side and in depth to see if there is any way to work with just one of them even. The annoyingly UI-centric format of the representation certainly doesn't help with that, that's one of the reasons why I've been putting this off for so long...

I don't know about your available time and plans for this PR, but I would like to see it merged eventually, even if it is not fully ready to be released, since it is a lot of code to review already.
We can always merge an early version of the provider and temporarily disable it by removing the changes to the provider registry for now.

const creditsEndpoint = item
.musicResponsiveListItemRenderer
.menu
.menuRenderer
Copy link
Owner

Choose a reason for hiding this comment

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

Unless we end up removing this code, we have to ensure that the menu exists:

Suggested change
.menuRenderer
?.menuRenderer

It is missing for unavailable tracks and currently breaks the conversion of such a release.

@wileyfoxyx
Copy link

wileyfoxyx commented Oct 1, 2025

If you go to artist's YouTube channel (whether it's their "Official Artist Channel" or a Topic channel) on main YouTube website and pick a release from there, it would still replace Art Tracks with corresponding music videos. Sometimes it can get worse, like in this single that has both original and "edit" version of a song but they both were replaced by the same music video.

Not sure if it's anyhow different for Premium users, since I'm not one myself.

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.

YouTube

4 participants