Compare commits

...

2 Commits

Author SHA1 Message Date
8b91a13095 release: version 0.5.0 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 19s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m44s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m52s
Build Docker image / deploy (push) Successful in 4m32s
2025-07-06 17:01:23 +02:00
27a5ca6b74 feat: add Navidrome song validator, refs #5 2025-07-06 17:01:07 +02:00
13 changed files with 152 additions and 23 deletions

View File

@ -4,11 +4,19 @@ Changelog
(unreleased) (unreleased)
------------ ------------
- Feat: add Navidrome song validator, refs #5. [Simon Diesenreiter]
0.4.2 (2025-06-25)
------------------
Fix Fix
~~~ ~~~
- Broken build, refs NOISSUE. [Simon Diesenreiter] - Broken build, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.4.1 (2025-06-25) 0.4.1 (2025-06-25)
------------------ ------------------
@ -294,10 +302,6 @@ Other
0.1.9 (2025-04-15) 0.1.9 (2025-04-15)
------------------ ------------------
0.1.8 (2025-04-15)
------------------
Fix Fix
~~~ ~~~
- Additional debug outputs refs NOISSUE. [Simon Diesenreiter] - Additional debug outputs refs NOISSUE. [Simon Diesenreiter]

View File

@ -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());

View File

@ -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);
} }

View File

@ -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}");
} }
} }

View File

@ -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);

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

View File

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

View File

@ -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();

View File

@ -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()

View File

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

View File

@ -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)

View File

@ -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()

View File

@ -1 +1 @@
0.4.2 0.5.0