feat: basic initial implementation of spotify client link validator and song submission form refs: NOISSUE
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m3s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m47s

This commit is contained in:
Simon Diesenreiter
2025-05-30 22:51:44 +02:00
parent 010316aa70
commit dbd83ebb6a
24 changed files with 485 additions and 35 deletions

View File

@@ -0,0 +1,8 @@
public interface ISongValidator
{
Task<Song> ValidateAsync(Uri songUri);
bool CanValidateUri(Uri songUri);
Task<bool> CanExtractSongMetadataAsync(Uri songUri);
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}