With RockPlayerApi orchestrating everything since article 5, only one piece is missing for it to work end to end: a real implementation of IMusicProviderAdapter. This article builds it using YouTube, and for now it will be the only one.
Why YouTube
RockRecommender's catalog is made of major label bands, Metallica, Iron Maiden, Nirvana, that kind of thing. For this example, I chose YouTube because it's the only way to play the full song from these bands without requiring whoever is using RockPlayer to have a paid subscription to some streaming service and log in. It just needs an API key to search.
There are other music APIs out there, Deezer and Spotify among them, but none of them delivers the full track from a major label band without going through login with a Premium account and playback through the service's own SDK, a kind of integration quite different from a simple HTTP search. That's how these services protect the licensing agreement they have with the labels. That's why they're left out of this series, not because they're worse, it's that they solve a different problem than the one we need here.
The API key
The YouTube Data API v3 requires a key, generated in the Google Cloud Console. The default daily quota is 10,000 units, and each search costs 100, which gives close to 100 searches per day on the free plan. That is exactly the math that justifies the cache from article 3, every search repeated without need is quota thrown away.
Creating a real key in the Google Cloud Console
Without these steps, every search comes back with a 403 error and the message "Method doesn't allow unregistered callers", which is Google's way of saying no key arrived with the request:
- Create or select a project at console.cloud.google.com.
- Enable the YouTube Data API v3 under "APIs & Services" > "Library".
- Create the key under "APIs & Services" > "Credentials" > "Create Credentials" > "API key".
- Restrict the key to just "YouTube Data API v3". If you want to restrict it by origin, use "IP addresses", never "HTTP referrer", since this call happens server-side, not from the browser.
- Paste the key into
YouTube.ApiKey, inappsettings.json.
The search
GET https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&maxResults=1&q=Metallica+Master+of+Puppets&key=YOUR_KEY
PlaybackSource grows into what the screen needs
The screen needs to know whether it should show a video or play audio with a cover, it needs a cover URL for the second case, and it needs the song id, band and title, so the player screen can show them and send the right id back with a like or dislike, without asking RockRecommender again. So PlaybackSource grows, and IMusicProviderAdapter grows with it, taking the whole NextSong instead of two loose strings:
public interface IMusicProviderAdapter
{
Task<Result<PlaybackSource>> FindAsync(NextSong song);
}
// YouTube always returns Video, AudioWithCover is here for a future provider
public enum PlaybackKind
{
Video,
AudioWithCover
}
public sealed record PlaybackSource(
Guid SongId,
string Band,
string Title,
PlaybackKind Kind,
string Url,
string? CoverImageUrl);
That one signature change also touches the single line in the orchestrator from article 5 that called this method, it becomes musicProviderAdapter.FindAsync(nextSong), passing the whole NextSong instead of pulling Band and Title out by hand.
The adapter
public sealed class YouTubeMusicProviderAdapter(
HttpClient httpClient,
IOptions<YouTubeOptions> options,
ILogger<YouTubeMusicProviderAdapter> logger) : IMusicProviderAdapter
{
public async Task<Result<PlaybackSource>> FindAsync(NextSong song)
{
var query = Uri.EscapeDataString($"{song.Band} {song.Title}");
var response = await httpClient.GetAsync(
$"search?part=snippet&type=video&maxResults=1&q={query}&key={options.Value.ApiKey}");
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
logger.LogWarning("YouTube search failed ({StatusCode}): {Body}", (int)response.StatusCode, errorBody);
return Result<PlaybackSource>.Unavailable("Could not reach the music provider right now.");
}
// PropertyNameCaseInsensitive because YouTube returns the fields in camelCase
var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var searchResponse = await response.Content.ReadFromJsonAsync<YouTubeSearchResponse>(jsonOptions);
var item = searchResponse?.Items.FirstOrDefault();
if (item is null)
return Result<PlaybackSource>.NotFound($"No YouTube video found for '{song.Band} - {song.Title}'.");
var embedUrl = $"https://www.youtube.com/embed/{item.Id.VideoId}";
return Result<PlaybackSource>.Success(new PlaybackSource(song.SongId, song.Band, song.Title, PlaybackKind.Video, embedUrl, item.Snippet.Thumbnails.High.Url));
}
}
During development, an important detail showed up: without logging, a configuration mistake (an empty key, for example) would return the whole body of Google's error response to the caller, exposing internal detail that doesn't help anyone who just wants to listen to a song. That's why ILogger comes in as one more dependency, alongside HttpClient and IOptions<YouTubeOptions>: diagnostic detail stays in the server log, the message to the client stays generic.
The types YouTubeSearchResponse, YouTubeSearchItem, YouTubeVideoId, YouTubeSnippet, YouTubeThumbnails and YouTubeThumbnail just mirror the format YouTube returns, one record inside another, each one with only the field that is actually used.
Configuration and registration
{
"YouTube": {
"ApiKey": ""
}
}
services.Configure<YouTubeOptions>(configuration.GetSection(YouTubeOptions.SectionName));
services.AddHttpClient<IMusicProviderAdapter, YouTubeMusicProviderAdapter>(client =>
{
client.BaseAddress = new Uri("https://www.googleapis.com/youtube/v3/");
});
With this, the backend is done: recommendation, cache and a real music provider all working together. The next article moves to the front-end, starting the RockPlayerWeb structure and the onboarding screen.