feat: implement song submission support, refs #5

This commit is contained in:
2025-06-05 00:14:53 +02:00
parent 0d2ec3712e
commit 220f4d7ffd
27 changed files with 943 additions and 191 deletions

View File

@@ -0,0 +1,23 @@
public class Base64UrlImageBuilder
{
public string ContentType { set; get; }
public string Url
{
set
{
var httpClient = new HttpClient();
var response = (httpClient.GetAsync(new Uri($"{value}"))).Result;
var bytes = (response.Content.ReadAsByteArrayAsync()).Result;
FileContents = Convert.ToBase64String(bytes);
}
}
private string FileContents { get; set; }
public override string ToString()
{
//return $"data:{ContentType};base64,{FileContents}";
return $"{FileContents}";
}
}

View File

@@ -5,4 +5,6 @@ public interface ISongValidator
bool CanValidateUri(Uri songUri);
Task<bool> CanExtractSongMetadataAsync(Uri songUri);
SongProvider GetSongProvider();
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
public class SongResolver
{
@@ -6,16 +7,20 @@ public class SongResolver
private readonly ILogger<SongResolver> logger;
public SongResolver(ILogger<SongResolver> logger)
private SpotifyApiClient spotifyApiClient;
public SongResolver(ILogger<SongResolver> logger, ILoggerFactory loggerFactory, SpotifyApiClient spotifyApiClient)
{
this.logger = logger;
this._songValidators = new List<ISongValidator>();
this.spotifyApiClient = spotifyApiClient;
foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
.Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base"))))
.Where(mytype => { return (mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.Split("`")[0].EndsWith("Base"))); }))
{
if (Activator.CreateInstance(mytype) is ISongValidator validator)
var typedLogger = loggerFactory.CreateLogger(mytype);
if (Activator.CreateInstance(mytype, typedLogger, spotifyApiClient) is ISongValidator validator)
{
logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name);
this._songValidators = this._songValidators.Append(validator);
@@ -65,4 +70,15 @@ public class SongResolver
return false;
}
public async Task<Base64UrlImageBuilder> GetPreviewImageAsync(string spotifyId)
{
var track = await spotifyApiClient.GetTrackByIdAsync(spotifyId);
var url = track.Album.Images.FirstOrDefault().Url;
return new Base64UrlImageBuilder()
{
Url = url,
ContentType = "image/jpeg"
};
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public abstract class SongValidatorBase : ISongValidator
{
@@ -8,9 +9,22 @@ public abstract class SongValidatorBase : ISongValidator
public abstract bool CanValidateUri(Uri songUri);
protected string LookupSpotifyId(string songName, string songArtist)
public abstract SongProvider GetSongProvider();
protected SpotifyApiClient _spotifyApiClient;
protected ILogger _logger;
public SongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient)
{
// TODO: Implement Spotify ID lookup logic
return songName + " by " + songArtist;
_spotifyApiClient = spotifyApiClient;
_logger = logger;
}
protected async Task<string> LookupSpotifyIdAsync(string songName, string songArtist)
{
var candidates = await _spotifyApiClient.GetTrackCandidatesAsync(songName, songArtist);
return candidates.Any() ? candidates[0].Id : "";
}
}

View File

@@ -7,24 +7,25 @@ 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 SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient)
{}
public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
return Task.FromResult(this.CanValidateUri(songUri));
}
public override SongProvider GetSongProvider()
{
return SongProvider.Spotify;
}
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 track = await _spotifyApiClient.GetTrackByIdAsync(trackIdMatch);
var song = new Song
{

View File

@@ -4,9 +4,17 @@ public abstract class UriBasedSongValidatorBase : SongValidatorBase
{
public abstract string UriValidatorRegex { get; }
public override bool CanValidateUri(Uri songUri)
public UriBasedSongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
{}
public Match GetUriMatch(Uri songUri)
{
var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
return regexp.Match(songUri.ToString()).Success;
return regexp.Match(songUri.ToString());
}
public override bool CanValidateUri(Uri songUri)
{
return GetUriMatch(songUri).Success;
}
}

View File

@@ -1,9 +1,22 @@
using AngleSharp;
using AngleSharp.Dom;
using YouTubeMusicAPI.Client;
public class YoutubeMusicValidator : UriBasedSongValidatorBase
{
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/playlist\?list=)([a-zA-Z0-9_-]+)";
private YouTubeMusicClient youtubeClient;
public YoutubeMusicValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
{
youtubeClient = new YouTubeMusicClient(logger, "AT", null, null, null);
}
public override SongProvider GetSongProvider()
{
return SongProvider.YoutubeMusic;
}
public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
@@ -12,35 +25,23 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
public override async Task<Song> ValidateAsync(Uri songUri)
{
var title = string.Empty;
var artist = string.Empty;
var match = this.GetUriMatch(songUri);
var playlistId = match.Groups[3].Value;
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 playlistResult = await youtubeClient.GetCommunityPlaylistInfoAsync(playlistId);
var songData = playlistResult.Songs[0];
var title = songData.Name;
var artist = songData.Artists[0].Name;
#pragma warning disable CS8604 // Possible null reference argument.
#pragma warning disable CS8604 // Possible null reference argument.
var song = new Song
{
Name = title,
Artist = artist,
Url = songUri.ToString(),
Provider = SongProvider.YouTube,
SpotifyId = this.LookupSpotifyId(title, artist)
Provider = SongProvider.YoutubeMusic,
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
};
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
return song;
}

View File

@@ -1,60 +1,47 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using YouTubeMusicAPI.Client;
public class YoutubeValidator : UriBasedSongValidatorBase
{
private YouTubeMusicClient youtubeClient;
public YoutubeValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
{
youtubeClient = new("AT");
}
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())))
{
#pragma warning disable CS8602 // Dereference of a possibly null reference.
var documentContents = (document.ChildNodes[1] as HtmlElement).InnerHtml;
#pragma warning restore CS8602 // Dereference of a possibly null reference.
var titleElement = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0];
var artistParentElement = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0];
return this.CanValidateUri(songUri);
}
return titleElement != null && artistParentElement != null && artistParentElement.Children.Length > 0;
}
}
public override SongProvider GetSongProvider()
{
return SongProvider.YouTube;
}
public override async Task<Song> ValidateAsync(Uri songUri)
{
var title = string.Empty;
var artist = string.Empty;
var match = this.GetUriMatch(songUri);
var songId = match.Groups[4].Value;
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 songData = await youtubeClient.GetSongVideoInfoAsync(songId);
var title = songData.Name;
var artist = songData.Artists[0].Name;
#pragma warning disable CS8604 // Possible null reference argument.
#pragma warning disable CS8604 // Possible null reference argument.
var song = new Song
{
Name = title,
Artist = artist,
Url = songUri.ToString(),
Provider = SongProvider.YouTube,
SpotifyId = this.LookupSpotifyId(title, artist)
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
};
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
return song;
}