feat: add Navidrome song validator, refs #5
This commit is contained in:
		@@ -11,10 +11,9 @@ public class PhoneClaimCodeProviderService
 | 
				
			|||||||
        _signalIntegration = signalIntegration;
 | 
					        _signalIntegration = signalIntegration;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static Random random = new Random();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static string RandomString(int length)
 | 
					    private static string RandomString(int length)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        Random random = new Random();
 | 
				
			||||||
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 | 
					        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 | 
				
			||||||
        return new string(Enumerable.Repeat(chars, length)
 | 
					        return new string(Enumerable.Repeat(chars, length)
 | 
				
			||||||
            .Select(s => s[random.Next(s.Length)]).ToArray());
 | 
					            .Select(s => s[random.Next(s.Length)]).ToArray());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -150,12 +150,12 @@ public class SignalIntegration
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        await this.SendMessageToUserAsync("Hi, my name is Proggy and I am your friendly neighborhood *Song of the Day* bot!", user.SignalMemberId);
 | 
					        await this.SendMessageToUserAsync("Hi, my name is Proggy and I am your friendly neighborhood *Song of the Day* bot!", user.SignalMemberId);
 | 
				
			||||||
        await this.SendMessageToUserAsync("You are receiving this message because you have been invited to a *Song of the Day* community group.", user.SignalMemberId);
 | 
					        await this.SendMessageToUserAsync("You are receiving this message because you have been invited to a *Song of the Day* community group.", user.SignalMemberId);
 | 
				
			||||||
        await this.SendMessageToUserAsync("In that community group I will pick a person at random each day at 8 AM and encourage them to share a song with the rest of the community.", user.SignalMemberId);
 | 
					        await this.SendMessageToUserAsync("In that community group I will pick a person at random every now and then and encourage them to share a song with the rest of the community.", user.SignalMemberId);
 | 
				
			||||||
        if (AppConfiguration.Instance.UseBotTag)
 | 
					        if (AppConfiguration.Instance.UseBotTag)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await this.SendMessageToUserAsync("You can always see which messages are sent by me rather than the community host by the **[Proggy]** tag at the beginning of the message", user.SignalMemberId);
 | 
					            await this.SendMessageToUserAsync("You can always see which messages are sent by me rather than the community host by the **[Proggy]** tag at the beginning of the message", user.SignalMemberId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        await this.SendMessageToUserAsync($"Not right now, but eventually you will be able to see more details about your community at {AppConfiguration.Instance.WebUIBaseURL}.", user.SignalMemberId);
 | 
					        await this.SendMessageToUserAsync($"You can find more details about your community at {AppConfiguration.Instance.WebUIBaseURL}.", user.SignalMemberId);
 | 
				
			||||||
        await this.SendMessageToUserAsync($"""You can navigate to {AppConfiguration.Instance.WebUIBaseURL + (AppConfiguration.Instance.WebUIBaseURL.EndsWith("/") ? "" : "/")}User/{user.UserId} to set your preferred display name for me to use.""", user.SignalMemberId);
 | 
					        await this.SendMessageToUserAsync($"""You can navigate to {AppConfiguration.Instance.WebUIBaseURL + (AppConfiguration.Instance.WebUIBaseURL.EndsWith("/") ? "" : "/")}User/{user.UserId} to set your preferred display name for me to use.""", user.SignalMemberId);
 | 
				
			||||||
        await this.SendMessageToUserAsync($"Now have fun and enjoy being a part of this community!", user.SignalMemberId);
 | 
					        await this.SendMessageToUserAsync($"Now have fun and enjoy being a part of this community!", user.SignalMemberId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -128,10 +128,10 @@ public class SongSubmissionModel : PageModel
 | 
				
			|||||||
            Url = suggestion.Song.Url,
 | 
					            Url = suggestion.Song.Url,
 | 
				
			||||||
            Base64Image = imageBuilder.ToString(),
 | 
					            Base64Image = imageBuilder.ToString(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        await signalIntegration.SendMessageToGroupAsync($"**{displayName}**'s " + dateString + $" is: \n\n {suggestion.Song.Url}", previewData);
 | 
					        await signalIntegration.SendMessageToGroupAsync($"**{displayName}**'s " + dateString + $" is: \n\n{suggestion.Song.Url}", previewData);
 | 
				
			||||||
        if (suggestion.HasUsedSuggestion)
 | 
					        if (suggestion.HasUsedSuggestion)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            await signalIntegration.SendMessageToGroupAsync($"The suggestion used for this pick was: \n\n **{suggestion.SuggestionHelper.Title}**'s \n\n {suggestion.SuggestionHelper.Description}");
 | 
					            await signalIntegration.SendMessageToGroupAsync($"The suggestion used for this pick was: \n\n**{suggestion.SuggestionHelper.Title}** \n\n{suggestion.SuggestionHelper.Description}");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ public interface ISongValidator
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    Task<Song> ValidateAsync(Uri songUri);
 | 
					    Task<Song> ValidateAsync(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bool CanValidateUri(Uri songUri);
 | 
					    Task<bool> CanValidateUriAsync(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Task<bool> CanExtractSongMetadataAsync(Uri songUri);
 | 
					    Task<bool> CanExtractSongMetadataAsync(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										121
									
								
								song_of_the_day/SongValidators/NavidromeValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								song_of_the_day/SongValidators/NavidromeValidator.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using AngleSharp;
 | 
				
			||||||
 | 
					using AngleSharp.Dom;
 | 
				
			||||||
 | 
					using AngleSharp.Html.Dom;
 | 
				
			||||||
 | 
					using YouTubeMusicAPI.Client;
 | 
				
			||||||
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					using Acornima.Ast;
 | 
				
			||||||
 | 
					using AngleSharp.Text;
 | 
				
			||||||
 | 
					using System.Text.RegularExpressions;
 | 
				
			||||||
 | 
					using System.Text.Json.Serialization;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class NavidromeValidator : SongValidatorBase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private YouTubeMusicClient youtubeClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public NavidromeValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        youtubeClient = new("AT");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public override async Task<bool> CanValidateUriAsync(Uri songUri)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Check if behind this URL there is a public navidrome share
 | 
				
			||||||
 | 
					        var config = Configuration.Default.WithDefaultLoader();
 | 
				
			||||||
 | 
					        var address = songUri.ToString();
 | 
				
			||||||
 | 
					        var context = BrowsingContext.New(config);
 | 
				
			||||||
 | 
					        var document = await context.OpenAsync(address);
 | 
				
			||||||
 | 
					        var titleCell = (document.DocumentElement.GetDescendants().First() as HtmlElement)
 | 
				
			||||||
 | 
					                                                .GetElementsByTagName("title").First();
 | 
				
			||||||
 | 
					        return "Navidrome".Equals(titleCell.TextContent);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return await this.CanValidateUriAsync(songUri);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public override SongProvider GetSongProvider()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return SongProvider.NavidromeSharedLink;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static Stream GenerateStreamFromString(string s)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var stream = new MemoryStream();
 | 
				
			||||||
 | 
					        var writer = new StreamWriter(stream);
 | 
				
			||||||
 | 
					        writer.Write(s);
 | 
				
			||||||
 | 
					        writer.Flush();
 | 
				
			||||||
 | 
					        stream.Position = 0;
 | 
				
			||||||
 | 
					        return stream;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public override async Task<Song> ValidateAsync(Uri songUri)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var config = Configuration.Default.WithDefaultLoader();
 | 
				
			||||||
 | 
					        var address = songUri.ToString();
 | 
				
			||||||
 | 
					        var context = BrowsingContext.New(config);
 | 
				
			||||||
 | 
					        var document = await context.OpenAsync(address);
 | 
				
			||||||
 | 
					        var infoScriptNode = document.GetElementsByTagName("script").Where(e => e.TextContent.Contains("__SHARE_INFO__")).First();
 | 
				
			||||||
 | 
					        var manipulatedValue = infoScriptNode.TextContent.Replace("window.__SHARE_INFO__ = \"", "").StripLeadingTrailingSpaces().StripLineBreaks();
 | 
				
			||||||
 | 
					        manipulatedValue = manipulatedValue.Remove(manipulatedValue.Length - 1);
 | 
				
			||||||
 | 
					        var infoScriptJsonData = Regex.Unescape(Regex.Unescape(manipulatedValue));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var title = string.Empty;
 | 
				
			||||||
 | 
					        var artist = string.Empty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        using (var stream = GenerateStreamFromString(infoScriptJsonData))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var jsonContent = await JsonSerializer.DeserializeAsync<NavidromeShareInfoData>(stream);
 | 
				
			||||||
 | 
					            title = jsonContent.Tracks[0].Title;
 | 
				
			||||||
 | 
					            artist = jsonContent.Tracks[0].Artist;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var song = new Song
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Name = title,
 | 
				
			||||||
 | 
					            Artist = artist,
 | 
				
			||||||
 | 
					            Url = songUri.ToString(),
 | 
				
			||||||
 | 
					            Provider = SongProvider.NavidromeSharedLink,
 | 
				
			||||||
 | 
					            SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return song;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class NavidromeShareInfoData
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    [JsonPropertyName("id")]
 | 
				
			||||||
 | 
					    public string Id { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("description")]
 | 
				
			||||||
 | 
					    public string Description { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("downloadable")]
 | 
				
			||||||
 | 
					    public bool Downloadable { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("tracks")]
 | 
				
			||||||
 | 
					    public List<NavidromeTrackInfoData> Tracks { get; set; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class NavidromeTrackInfoData
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    [JsonPropertyName("id")]
 | 
				
			||||||
 | 
					    public string Id { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("title")]
 | 
				
			||||||
 | 
					    public string Title { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("artist")]
 | 
				
			||||||
 | 
					    public string Artist { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("album")]
 | 
				
			||||||
 | 
					    public string Album { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("updatedAt")]
 | 
				
			||||||
 | 
					    public DateTime UpdatedAt { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [JsonPropertyName("duration")]
 | 
				
			||||||
 | 
					    public float Duration { get; set; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using SpotifyAPI.Web;
 | 
					using SpotifyAPI.Web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,7 +33,7 @@ public class SongResolver
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach (var validator in _songValidators)
 | 
					        foreach (var validator in _songValidators)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (validator.CanValidateUri(songUri))
 | 
					            if (await validator.CanValidateUriAsync(songUri))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (!await validator.CanExtractSongMetadataAsync(songUri))
 | 
					                if (!await validator.CanExtractSongMetadataAsync(songUri))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -62,7 +63,7 @@ public class SongResolver
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach (var validator in _songValidators)
 | 
					        foreach (var validator in _songValidators)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (validator.CanValidateUri(songUri))
 | 
					            if (validator.CanValidateUriAsync(songUri).Result)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ public abstract class SongValidatorBase : ISongValidator
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public abstract Task<bool> CanExtractSongMetadataAsync(Uri songUri);
 | 
					    public abstract Task<bool> CanExtractSongMetadataAsync(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public abstract bool CanValidateUri(Uri songUri);
 | 
					    public abstract Task<bool> CanValidateUriAsync(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public abstract SongProvider GetSongProvider();
 | 
					    public abstract SongProvider GetSongProvider();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,9 +10,9 @@ public class SpotifyValidator : UriBasedSongValidatorBase
 | 
				
			|||||||
    public SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient)
 | 
					    public SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient)
 | 
				
			||||||
    { }
 | 
					    { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
					    public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return Task.FromResult(this.CanValidateUri(songUri));
 | 
					        return await this.CanValidateUriAsync(songUri);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override SongProvider GetSongProvider()
 | 
					    public override SongProvider GetSongProvider()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,12 @@ public abstract class UriBasedSongValidatorBase : SongValidatorBase
 | 
				
			|||||||
        return regexp.Match(songUri.ToString());
 | 
					        return regexp.Match(songUri.ToString());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override bool CanValidateUri(Uri songUri)
 | 
					    public override async Task<bool> CanValidateUriAsync(Uri songUri)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return GetUriMatch(songUri).Success;
 | 
					        var result = await Task.Run(() =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return GetUriMatch(songUri).Success;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -18,9 +18,9 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
 | 
				
			|||||||
        return SongProvider.YoutubeMusic;
 | 
					        return SongProvider.YoutubeMusic;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
					    public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return Task.FromResult(this.CanValidateUri(songUri));
 | 
					        return await this.CanValidateUriAsync(songUri);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override async Task<Song> ValidateAsync(Uri songUri)
 | 
					    public override async Task<Song> ValidateAsync(Uri songUri)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,9 +14,9 @@ public class YoutubeValidator : UriBasedSongValidatorBase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public override string UriValidatorRegex => @"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
 | 
					    public override string UriValidatorRegex => @"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
					    public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return Task.FromResult(this.CanValidateUri(songUri));
 | 
					        return await this.CanValidateUriAsync(songUri);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override SongProvider GetSongProvider()
 | 
					    public override SongProvider GetSongProvider()
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user