diff --git a/song_of_the_day/Auth/PhoneClaimCodeProviderService.cs b/song_of_the_day/Auth/PhoneClaimCodeProviderService.cs index 741644d..5ae3e93 100644 --- a/song_of_the_day/Auth/PhoneClaimCodeProviderService.cs +++ b/song_of_the_day/Auth/PhoneClaimCodeProviderService.cs @@ -11,10 +11,9 @@ public class PhoneClaimCodeProviderService _signalIntegration = signalIntegration; } - private static Random random = new Random(); - private static string RandomString(int length) { + Random random = new Random(); const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; return new string(Enumerable.Repeat(chars, length) .Select(s => s[random.Next(s.Length)]).ToArray()); diff --git a/song_of_the_day/MessengerIntegration/SignalIntegration.cs b/song_of_the_day/MessengerIntegration/SignalIntegration.cs index 9af1311..3bf2953 100644 --- a/song_of_the_day/MessengerIntegration/SignalIntegration.cs +++ b/song_of_the_day/MessengerIntegration/SignalIntegration.cs @@ -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("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) { 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($"Now have fun and enjoy being a part of this community!", user.SignalMemberId); } diff --git a/song_of_the_day/Pages/SongSubmission.cshtml.cs b/song_of_the_day/Pages/SongSubmission.cshtml.cs index e6d5822..e6b6e13 100644 --- a/song_of_the_day/Pages/SongSubmission.cshtml.cs +++ b/song_of_the_day/Pages/SongSubmission.cshtml.cs @@ -128,10 +128,10 @@ public class SongSubmissionModel : PageModel Url = suggestion.Song.Url, 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) { - 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}"); } } diff --git a/song_of_the_day/SongValidators/ISongValidator.cs b/song_of_the_day/SongValidators/ISongValidator.cs index 72b3789..6f86e1e 100644 --- a/song_of_the_day/SongValidators/ISongValidator.cs +++ b/song_of_the_day/SongValidators/ISongValidator.cs @@ -2,7 +2,7 @@ public interface ISongValidator { Task ValidateAsync(Uri songUri); - bool CanValidateUri(Uri songUri); + Task CanValidateUriAsync(Uri songUri); Task CanExtractSongMetadataAsync(Uri songUri); diff --git a/song_of_the_day/SongValidators/NavidromeValidator.cs b/song_of_the_day/SongValidators/NavidromeValidator.cs new file mode 100644 index 0000000..77b7db1 --- /dev/null +++ b/song_of_the_day/SongValidators/NavidromeValidator.cs @@ -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 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 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 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(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 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; } +} \ No newline at end of file diff --git a/song_of_the_day/SongValidators/SongResolver.cs b/song_of_the_day/SongValidators/SongResolver.cs index 4fd3be4..a5465a7 100644 --- a/song_of_the_day/SongValidators/SongResolver.cs +++ b/song_of_the_day/SongValidators/SongResolver.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SpotifyAPI.Web; @@ -32,7 +33,7 @@ public class SongResolver { foreach (var validator in _songValidators) { - if (validator.CanValidateUri(songUri)) + if (await validator.CanValidateUriAsync(songUri)) { if (!await validator.CanExtractSongMetadataAsync(songUri)) { @@ -62,7 +63,7 @@ public class SongResolver { foreach (var validator in _songValidators) { - if (validator.CanValidateUri(songUri)) + if (validator.CanValidateUriAsync(songUri).Result) { return true; } diff --git a/song_of_the_day/SongValidators/SongValidatorBase.cs b/song_of_the_day/SongValidators/SongValidatorBase.cs index 00cf8ab..2cb389e 100644 --- a/song_of_the_day/SongValidators/SongValidatorBase.cs +++ b/song_of_the_day/SongValidators/SongValidatorBase.cs @@ -7,7 +7,7 @@ public abstract class SongValidatorBase : ISongValidator public abstract Task CanExtractSongMetadataAsync(Uri songUri); - public abstract bool CanValidateUri(Uri songUri); + public abstract Task CanValidateUriAsync(Uri songUri); public abstract SongProvider GetSongProvider(); diff --git a/song_of_the_day/SongValidators/SpotifyValidator.cs b/song_of_the_day/SongValidators/SpotifyValidator.cs index 503b545..6d4af58 100644 --- a/song_of_the_day/SongValidators/SpotifyValidator.cs +++ b/song_of_the_day/SongValidators/SpotifyValidator.cs @@ -10,9 +10,9 @@ public class SpotifyValidator : UriBasedSongValidatorBase public SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient) { } - public override Task CanExtractSongMetadataAsync(Uri songUri) + public override async Task CanExtractSongMetadataAsync(Uri songUri) { - return Task.FromResult(this.CanValidateUri(songUri)); + return await this.CanValidateUriAsync(songUri); } public override SongProvider GetSongProvider() diff --git a/song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs b/song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs index 325324c..829a82c 100644 --- a/song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs +++ b/song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs @@ -13,8 +13,12 @@ public abstract class UriBasedSongValidatorBase : SongValidatorBase return regexp.Match(songUri.ToString()); } - public override bool CanValidateUri(Uri songUri) + public override async Task CanValidateUriAsync(Uri songUri) { - return GetUriMatch(songUri).Success; + var result = await Task.Run(() => + { + return GetUriMatch(songUri).Success; + }); + return result; } } \ No newline at end of file diff --git a/song_of_the_day/SongValidators/YoutubeMusicValidator.cs b/song_of_the_day/SongValidators/YoutubeMusicValidator.cs index 00df121..fa9bdb4 100644 --- a/song_of_the_day/SongValidators/YoutubeMusicValidator.cs +++ b/song_of_the_day/SongValidators/YoutubeMusicValidator.cs @@ -18,9 +18,9 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase return SongProvider.YoutubeMusic; } - public override Task CanExtractSongMetadataAsync(Uri songUri) + public override async Task CanExtractSongMetadataAsync(Uri songUri) { - return Task.FromResult(this.CanValidateUri(songUri)); + return await this.CanValidateUriAsync(songUri); } public override async Task ValidateAsync(Uri songUri) diff --git a/song_of_the_day/SongValidators/YoutubeValidator.cs b/song_of_the_day/SongValidators/YoutubeValidator.cs index 69a3986..68c813f 100644 --- a/song_of_the_day/SongValidators/YoutubeValidator.cs +++ b/song_of_the_day/SongValidators/YoutubeValidator.cs @@ -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 Task CanExtractSongMetadataAsync(Uri songUri) + public override async Task CanExtractSongMetadataAsync(Uri songUri) { - return Task.FromResult(this.CanValidateUri(songUri)); + return await this.CanValidateUriAsync(songUri); } public override SongProvider GetSongProvider()