feat: basic initial implementation of spotify client link validator and song submission form refs: NOISSUE
This commit is contained in:
8
song_of_the_day/SongValidators/ISongValidator.cs
Normal file
8
song_of_the_day/SongValidators/ISongValidator.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
public interface ISongValidator
|
||||
{
|
||||
Task<Song> ValidateAsync(Uri songUri);
|
||||
|
||||
bool CanValidateUri(Uri songUri);
|
||||
|
||||
Task<bool> CanExtractSongMetadataAsync(Uri songUri);
|
||||
}
|
66
song_of_the_day/SongValidators/SongResolver.cs
Normal file
66
song_of_the_day/SongValidators/SongResolver.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class SongResolver
|
||||
{
|
||||
private readonly IEnumerable<ISongValidator> _songValidators;
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
public SongResolver()
|
||||
{
|
||||
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
|
||||
this.logger = factory.CreateLogger("SongResolver");
|
||||
|
||||
this._songValidators = new List<ISongValidator>();
|
||||
|
||||
foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base")))) {
|
||||
if (Activator.CreateInstance(mytype) is ISongValidator validator)
|
||||
{
|
||||
logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name);
|
||||
this._songValidators = this._songValidators.Append(validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Song> ResolveSongAsync(Uri songUri)
|
||||
{
|
||||
foreach (var validator in _songValidators)
|
||||
{
|
||||
if (validator.CanValidateUri(songUri))
|
||||
{
|
||||
if (!await validator.CanExtractSongMetadataAsync(songUri))
|
||||
{
|
||||
this.logger.LogWarning("Cannot extract metadata for song URI: {SongUri}", songUri);
|
||||
return new Song {
|
||||
Artist = "Unknown Artist",
|
||||
Name = "Unknown Title",
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.PlainHttp,
|
||||
};
|
||||
}
|
||||
return await validator.ValidateAsync(songUri);
|
||||
}
|
||||
}
|
||||
|
||||
return new Song {
|
||||
Artist = "Unknown Artist",
|
||||
Name = "Unknown Title",
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.PlainHttp,
|
||||
};
|
||||
}
|
||||
|
||||
public bool CanValidate(Uri songUri)
|
||||
{
|
||||
foreach (var validator in _songValidators)
|
||||
{
|
||||
if (validator.CanValidateUri(songUri))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
16
song_of_the_day/SongValidators/SongValidatorBase.cs
Normal file
16
song_of_the_day/SongValidators/SongValidatorBase.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public abstract class SongValidatorBase : ISongValidator
|
||||
{
|
||||
public abstract Task<Song> ValidateAsync(Uri songUri);
|
||||
|
||||
public abstract Task<bool> CanExtractSongMetadataAsync(Uri songUri);
|
||||
|
||||
public abstract bool CanValidateUri(Uri songUri);
|
||||
|
||||
protected string LookupSpotifyId(string songName, string songArtist)
|
||||
{
|
||||
// TODO: Implement Spotify ID lookup logic
|
||||
return songName + " by " + songArtist;
|
||||
}
|
||||
}
|
40
song_of_the_day/SongValidators/SpotifyValidator.cs
Normal file
40
song_of_the_day/SongValidators/SpotifyValidator.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Dom;
|
||||
|
||||
public class SpotifyValidator : UriBasedSongValidatorBase
|
||||
{
|
||||
public override string UriValidatorRegex => @"^(https?://)?open.spotify.com/track/([a-zA-Z0-9_-]{22})(\?si=[a-zA-Z0-9_-]+)?$";
|
||||
|
||||
private SpotifyApiClient spotifyApiClient;
|
||||
|
||||
public SpotifyValidator()
|
||||
{
|
||||
spotifyApiClient = new SpotifyApiClient();
|
||||
}
|
||||
|
||||
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||
{
|
||||
return this.CanValidateUri(songUri);
|
||||
}
|
||||
|
||||
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||
{
|
||||
var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
|
||||
var trackIdMatch = regexp.Match(songUri.ToString()).Groups[2].Value;
|
||||
|
||||
var track = await spotifyApiClient.GetTrackByIdAsync(trackIdMatch);
|
||||
|
||||
var song = new Song
|
||||
{
|
||||
Name = track.Name,
|
||||
Artist = track.Artists.FirstOrDefault()?.Name ?? "Unknown Artist",
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.Spotify,
|
||||
SpotifyId = trackIdMatch
|
||||
};
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
12
song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
Normal file
12
song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public abstract class UriBasedSongValidatorBase : SongValidatorBase
|
||||
{
|
||||
public abstract string UriValidatorRegex { get; }
|
||||
|
||||
public override bool CanValidateUri(Uri songUri)
|
||||
{
|
||||
var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
|
||||
return regexp.Match(songUri.ToString()).Success;
|
||||
}
|
||||
}
|
43
song_of_the_day/SongValidators/YoutubeMusicValidator.cs
Normal file
43
song_of_the_day/SongValidators/YoutubeMusicValidator.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
|
||||
public class YoutubeMusicValidator : UriBasedSongValidatorBase
|
||||
{
|
||||
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
|
||||
|
||||
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||
{
|
||||
return this.CanValidateUri(songUri);
|
||||
}
|
||||
|
||||
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||
{
|
||||
var title = string.Empty;
|
||||
var artist = string.Empty;
|
||||
|
||||
using(HttpClient httpClient = new HttpClient())
|
||||
{
|
||||
var response = await httpClient.GetAsync(songUri);
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
|
||||
{
|
||||
// document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("song-title")[0].innerHTML
|
||||
title = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".song-title")?.InnerHtml;
|
||||
// document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("byline")[0].innerHTML
|
||||
artist = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".byline")?.InnerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
var song = new Song
|
||||
{
|
||||
Name = title,
|
||||
Artist = artist,
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.YouTube,
|
||||
SpotifyId = this.LookupSpotifyId(title, artist)
|
||||
};
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
55
song_of_the_day/SongValidators/YoutubeValidator.cs
Normal file
55
song_of_the_day/SongValidators/YoutubeValidator.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Dom;
|
||||
|
||||
public class YoutubeValidator : UriBasedSongValidatorBase
|
||||
{
|
||||
public override string UriValidatorRegex => @"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
|
||||
|
||||
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||
{
|
||||
using(HttpClient httpClient = new HttpClient())
|
||||
{
|
||||
var response = await httpClient.GetAsync(songUri);
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
|
||||
{
|
||||
var documentContents = (document.ChildNodes[1] as HtmlElement).InnerHtml;
|
||||
var titleElement = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0];
|
||||
var artistParentElement = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0];
|
||||
|
||||
return titleElement != null && artistParentElement != null && artistParentElement.Children.Length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||
{
|
||||
var title = string.Empty;
|
||||
var artist = string.Empty;
|
||||
|
||||
using(HttpClient httpClient = new HttpClient())
|
||||
{
|
||||
var response = await httpClient.GetAsync(songUri);
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
|
||||
{
|
||||
title = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0]?.InnerHtml;
|
||||
artist = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0]?.Children[0]?.InnerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
var song = new Song
|
||||
{
|
||||
Name = title,
|
||||
Artist = artist,
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.YouTube,
|
||||
SpotifyId = this.LookupSpotifyId(title, artist)
|
||||
};
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user