feat: basic initial implementation of spotify client link validator and song submission form refs: NOISSUE
This commit is contained in:
		
							
								
								
									
										4
									
								
								song_of_the_day/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								song_of_the_day/.editorconfig
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					[*.cs]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
 | 
				
			||||||
 | 
					dotnet_diagnostic.CS8981.severity = none
 | 
				
			||||||
@@ -10,6 +10,6 @@ public class LdapAuthenticationService : IAuthenticationService
 | 
				
			|||||||
    public bool Authenticate(string username, string password)
 | 
					    public bool Authenticate(string username, string password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ldapInstance = LdapIntegration.Instance;
 | 
					        var ldapInstance = LdapIntegration.Instance;
 | 
				
			||||||
        return ldapInstance.TestLogin(username, password);
 | 
					        return ldapInstance == null ? false : ldapInstance.TestLogin(username, password);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -18,7 +18,7 @@ public class PhoneClaimCodeProviderService
 | 
				
			|||||||
            .Select(s => s[random.Next(s.Length)]).ToArray());
 | 
					            .Select(s => s[random.Next(s.Length)]).ToArray());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
 | 
					    public async void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var generatedCode = string.Empty;
 | 
					        var generatedCode = string.Empty;
 | 
				
			||||||
        if (IsCodeGeneratedForUser(username))
 | 
					        if (IsCodeGeneratedForUser(username))
 | 
				
			||||||
@@ -32,7 +32,7 @@ public class PhoneClaimCodeProviderService
 | 
				
			|||||||
            _phoneClaimNumbers[username] = phoneNumber;
 | 
					            _phoneClaimNumbers[username] = phoneNumber;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        SignalIntegration.Instance.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
 | 
					        await SignalIntegration.Instance.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public string ValidateClaimCodeForUser(string code, string username)
 | 
					    public string ValidateClaimCodeForUser(string code, string username)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,8 @@ public class AppConfiguration
 | 
				
			|||||||
        var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
 | 
					        var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
 | 
				
			||||||
        var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
 | 
					        var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
 | 
				
			||||||
        var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND");
 | 
					        var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND");
 | 
				
			||||||
 | 
					        this.SpotifyClientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") ?? "0c59b625470b4ad1b70743e0254d17fd";
 | 
				
			||||||
 | 
					        this.SpotifyClientSecret = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET") ?? "04daaebd42fc47909c5cbd1f5cf23555";
 | 
				
			||||||
        this.LDAPConfig = new ConfigurationAD()
 | 
					        this.LDAPConfig = new ConfigurationAD()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
 | 
					            Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
 | 
				
			||||||
@@ -83,6 +85,16 @@ public class AppConfiguration
 | 
				
			|||||||
        get; private set;
 | 
					        get; private set;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string SpotifyClientId
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        get; private set;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string SpotifyClientSecret
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        get; private set;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public bool UseBotTag
 | 
					    public bool UseBotTag
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        get; private set;
 | 
					        get; private set;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ public class AuthController : Controller
 | 
				
			|||||||
    public async Task<IActionResult> Login(string username, string password)
 | 
					    public async Task<IActionResult> Login(string username, string password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
 | 
					        var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
 | 
				
			||||||
        if (ldapService.Authenticate(username, password))
 | 
					        if (ldapService != null && ldapService.Authenticate(username, password))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var claims = new[] { new Claim(ClaimTypes.Name, username) };
 | 
					            var claims = new[] { new Claim(ClaimTypes.Name, username) };
 | 
				
			||||||
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
 | 
					            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,4 +6,6 @@ public class Song
 | 
				
			|||||||
    public string? Name { get; set; }
 | 
					    public string? Name { get; set; }
 | 
				
			||||||
    public string? Artist { get; set; }
 | 
					    public string? Artist { get; set; }
 | 
				
			||||||
    public string? Url { get; set; }
 | 
					    public string? Url { get; set; }
 | 
				
			||||||
 | 
					    public SongProvider? Provider { get; set; }
 | 
				
			||||||
 | 
					    public string? SpotifyId { get; set; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								song_of_the_day/Data/SongProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								song_of_the_day/Data/SongProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					public enum SongProvider
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    Spotify,
 | 
				
			||||||
 | 
					    YouTube,
 | 
				
			||||||
 | 
					    YoutubeMusic,
 | 
				
			||||||
 | 
					    SoundCloud,
 | 
				
			||||||
 | 
					    Bandcamp,
 | 
				
			||||||
 | 
					    PlainHttp,
 | 
				
			||||||
 | 
					    NavidromeSharedLink,
 | 
				
			||||||
 | 
					    Other
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								song_of_the_day/GlobalSuppressions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								song_of_the_day/GlobalSuppressions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					// This file is used by Code Analysis to maintain SuppressMessage
 | 
				
			||||||
 | 
					// attributes that are applied to this project.
 | 
				
			||||||
 | 
					// Project-level suppressions either have no target or are given
 | 
				
			||||||
 | 
					// a specific target and scoped to a namespace, type, member, etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using System.Diagnostics.CodeAnalysis;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~P:sotd.Pages.UnclaimedPhoneNumbersModel.userId")]
 | 
				
			||||||
 | 
					[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~P:sotd.Pages.UserModel.userId")]
 | 
				
			||||||
 | 
					[assembly: SuppressMessage("Compiler", "CS8981:The type name only contains lower-cased ascii characters. Such names may become reserved for the language.", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.additionaldataforsongsubmissions")]
 | 
				
			||||||
 | 
					[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.additionaldataforsongsubmissions")]
 | 
				
			||||||
 | 
					[assembly: SuppressMessage("Compiler", "CS8981:The type name only contains lower-cased ascii characters. Such names may become reserved for the language.", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.keeptrackofusersoickedforsubmission")]
 | 
				
			||||||
 | 
					[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.keeptrackofusersoickedforsubmission")]
 | 
				
			||||||
@@ -7,8 +7,13 @@ public class SignalIntegration
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public static SignalIntegration? Instance;
 | 
					    public static SignalIntegration? Instance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private readonly ILogger logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public SignalIntegration(string uri, int port, string phoneNumber)
 | 
					    public SignalIntegration(string uri, int port, string phoneNumber)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
 | 
				
			||||||
 | 
					        this.logger = factory.CreateLogger("SignalIntegration");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var http = new HttpClient()
 | 
					        var http = new HttpClient()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            BaseAddress = new Uri(uri + ":" + port)
 | 
					            BaseAddress = new Uri(uri + ":" + port)
 | 
				
			||||||
@@ -24,17 +29,19 @@ public class SignalIntegration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public async Task ListGroupsAsync()
 | 
					    public async Task ListGroupsAsync()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        logger.LogDebug("Listing all groups for phone number: {PhoneNumber}", this.phoneNumber);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            ICollection<song_of_the_day.GroupEntry> groupEntries = await apiClient.GroupsAllAsync(this.phoneNumber);
 | 
					            ICollection<song_of_the_day.GroupEntry> groupEntries = await apiClient.GroupsAllAsync(this.phoneNumber);
 | 
				
			||||||
            foreach (var group in groupEntries)
 | 
					            foreach (var group in groupEntries)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Console.WriteLine($"{group.Name} {group.Id}");
 | 
					                logger.LogDebug($"  {group.Name} {group.Id}");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Exception (ListGroupsAsync): " + ex.Message);
 | 
					            logger.LogError("Exception (ListGroupsAsync): " + ex.Message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +59,7 @@ public class SignalIntegration
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Exception (SendMessageToGroupAsync): " + ex.Message);
 | 
					            logger.LogError("Exception (SendMessageToGroupAsync): " + ex.Message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -70,7 +77,7 @@ public class SignalIntegration
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Exception (SendMessageToUserAsync): " + ex.Message);
 | 
					            logger.LogError("Exception (SendMessageToUserAsync): " + ex.Message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -97,7 +104,7 @@ public class SignalIntegration
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Exception (GetMemberListAsync): " + ex.Message);
 | 
					            logger.LogError("Exception (GetMemberListAsync): " + ex.Message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return new List<string>();
 | 
					        return new List<string>();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -117,7 +124,7 @@ public class SignalIntegration
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Console.WriteLine("Exception (GetContactAsync): " + ex.Message);
 | 
					            logger.LogError("Exception (GetContactAsync): " + ex.Message);
 | 
				
			||||||
            return new ListContactsResponse();
 | 
					            return new ListContactsResponse();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								song_of_the_day/Pages/Shared/UpdateInputText.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								song_of_the_day/Pages/Shared/UpdateInputText.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					@inherits Microsoft.AspNetCore.Components.Forms.InputText
 | 
				
			||||||
 | 
					<input @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />
 | 
				
			||||||
@@ -43,6 +43,12 @@
 | 
				
			|||||||
                                <a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
 | 
					                                <a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
 | 
				
			||||||
                            </li>
 | 
					                            </li>
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        @if (this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            <li class="nav-item">
 | 
				
			||||||
 | 
					                                <a class="nav-link text-dark" asp-area="" asp-page="/SubmitSongs">Submit Songs</a>
 | 
				
			||||||
 | 
					                            </li>
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    </ul>
 | 
					                    </ul>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								song_of_the_day/Pages/Shared/_SongPartial.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								song_of_the_day/Pages/Shared/_SongPartial.cshtml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					@model Song
 | 
				
			||||||
 | 
					<label asp-for="Name">Name:</label>
 | 
				
			||||||
 | 
					<input asp-for="Name" />
 | 
				
			||||||
 | 
					<label asp-for="Artist">Artist:</label>
 | 
				
			||||||
 | 
					<input asp-for="Artist" />
 | 
				
			||||||
 | 
					<label asp-for="SpotifyId">Spotify ID:</label>
 | 
				
			||||||
 | 
					<input asp-for="SpotifyId" />
 | 
				
			||||||
							
								
								
									
										25
									
								
								song_of_the_day/Pages/SubmitSongs.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								song_of_the_day/Pages/SubmitSongs.cshtml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					@page
 | 
				
			||||||
 | 
					@model SubmitSongsModel
 | 
				
			||||||
 | 
					@{
 | 
				
			||||||
 | 
					    ViewData["Title"] = "Submit Songs";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="text-left">
 | 
				
			||||||
 | 
					    <form method="post">
 | 
				
			||||||
 | 
					        <label asp-for="SubmitUrl" >Song Url:</label>
 | 
				
			||||||
 | 
					        <input asp-for="SubmitUrl" oninput="Update(this)" />
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					        <form method="post">
 | 
				
			||||||
 | 
					            <div id="songdata">
 | 
				
			||||||
 | 
					                <partial name="_SongPartial" model="@Model.SongData" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <input type="submit" title="Submit" value="Submit" disabled="CanSubmit" />
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    function Update(t) {
 | 
				
			||||||
 | 
					        var url = '?handler=Update&&SubmitUrl=' + $(t).val();
 | 
				
			||||||
 | 
					        $('#songdata').load(url)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										71
									
								
								song_of_the_day/Pages/SubmitSongs.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								song_of_the_day/Pages/SubmitSongs.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					using Microsoft.AspNetCore.Components;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Mvc.RazorPages;
 | 
				
			||||||
 | 
					using Microsoft.VisualBasic;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace sotd.Pages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class SubmitSongsModel : PageModel
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private readonly ILogger<UserModel> _logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private SongResolver songResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private string _submitUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public SubmitSongsModel(ILogger<UserModel> logger, SongResolver songResolver)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _logger = logger;
 | 
				
			||||||
 | 
					        this.songResolver = songResolver;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [BindProperty]
 | 
				
			||||||
 | 
					    public bool IsValidUrl { get; set; } = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [BindProperty]
 | 
				
			||||||
 | 
					    public string SubmitUrl { 
 | 
				
			||||||
 | 
					        get {
 | 
				
			||||||
 | 
					            return _submitUrl;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        set {
 | 
				
			||||||
 | 
					            _submitUrl = value.ToString();
 | 
				
			||||||
 | 
					            Uri? newValue = default;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                newValue = new Uri(_submitUrl);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            catch (UriFormatException)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                IsValidUrl = false;
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            IsValidUrl = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(this.songResolver.CanValidate(newValue))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                this.SongData = this.songResolver.ResolveSongAsync(newValue).Result;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [BindProperty]
 | 
				
			||||||
 | 
					    public bool CanSubmit { get {
 | 
				
			||||||
 | 
					        return !string.IsNullOrEmpty(SongData?.Artist) && ! string.IsNullOrEmpty(SongData?.Name);
 | 
				
			||||||
 | 
					    } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [BindProperty]
 | 
				
			||||||
 | 
					    public Song SongData { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void OnPost()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Todo implement save submission
 | 
				
			||||||
 | 
					        var x = SongData.Name;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public IActionResult OnGetUpdate()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var songUrl = Request.Query["SubmitUrl"];
 | 
				
			||||||
 | 
					        this.SubmitUrl = songUrl.ToString();
 | 
				
			||||||
 | 
					        return Partial("_SongPartial", SongData);;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -18,7 +18,10 @@ LdapIntegration.Instance = new LdapIntegration(AppConfiguration.Instance.LDAPCon
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var builder = WebApplication.CreateBuilder(args);
 | 
					var builder = WebApplication.CreateBuilder(args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Console.WriteLine("Setting up user check timer");
 | 
					using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
 | 
				
			||||||
 | 
					var logger = factory.CreateLogger("SongResolver");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger.LogTrace("Setting up user check timer");
 | 
				
			||||||
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
					var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
				
			||||||
userCheckTimer.OnOccurence += async (s, ea) =>
 | 
					userCheckTimer.OnOccurence += async (s, ea) =>
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -31,9 +34,9 @@ userCheckTimer.OnOccurence += async (s, ea) =>
 | 
				
			|||||||
        if (foundUser == null)
 | 
					        if (foundUser == null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId);
 | 
					            var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId);
 | 
				
			||||||
            Console.WriteLine("New user:");
 | 
					            logger.LogDebug("New user:");
 | 
				
			||||||
            Console.WriteLine($"   Name: {newUserContact.Name}");
 | 
					            logger.LogDebug($"   Name: {newUserContact.Name}");
 | 
				
			||||||
            Console.WriteLine($"   MemberId: {memberId}");
 | 
					            logger.LogDebug($"   MemberId: {memberId}");
 | 
				
			||||||
            User newUser = new User()
 | 
					            User newUser = new User()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Name = newUserContact.Name,
 | 
					                Name = newUserContact.Name,
 | 
				
			||||||
@@ -55,9 +58,8 @@ userCheckTimer.OnOccurence += async (s, ea) =>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    await dci.DisposeAsync();
 | 
					    await dci.DisposeAsync();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
userCheckTimer.Start();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Console.WriteLine("Setting up user intro timer");
 | 
					logger.LogTrace("Setting up user intro timer");
 | 
				
			||||||
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
					var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
				
			||||||
userIntroTimer.OnOccurence += async (s, ea) =>
 | 
					userIntroTimer.OnOccurence += async (s, ea) =>
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -83,10 +85,9 @@ userIntroTimer.OnOccurence += async (s, ea) =>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    await dci.DisposeAsync();
 | 
					    await dci.DisposeAsync();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
userIntroTimer.Start();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Console.WriteLine("Setting up pick of the day timer");
 | 
					logger.LogTrace("Setting up pick of the day timer");
 | 
				
			||||||
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
 | 
					var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
 | 
				
			||||||
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
 | 
					pickOfTheDayTimer.OnOccurence += async (s, ea) =>
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -94,32 +95,32 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();
 | 
					    var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lastSong != null && lastSong.Date >= DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
 | 
					    if (lastSong != null && lastSong.Date > DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Console.WriteLine("Skipping pick of the day today!");
 | 
					        logger.LogWarning("Skipping pick of the day today!");
 | 
				
			||||||
        await dci.DisposeAsync();
 | 
					        await dci.DisposeAsync();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
 | 
					    if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Console.WriteLine("Unable to properly initialize DB context!");
 | 
					        logger.LogError("Unable to properly initialize DB context!");
 | 
				
			||||||
        await dci.DisposeAsync();
 | 
					        await dci.DisposeAsync();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
 | 
					    var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
 | 
				
			||||||
    if (!potentialUsers.Any())
 | 
					    if (!potentialUsers.Any())
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Console.WriteLine("Resetting suggestion count on users before resuming");
 | 
					        logger.LogTrace("Resetting suggestion count on users before resuming");
 | 
				
			||||||
        await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
 | 
					        await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
 | 
				
			||||||
        await dci.SaveChangesAsync();
 | 
					        await dci.SaveChangesAsync();
 | 
				
			||||||
        potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
 | 
					        potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    Console.WriteLine("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
 | 
					    logger.LogDebug("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
 | 
				
			||||||
    var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
 | 
					    var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
 | 
				
			||||||
    if (luckyUser == null)
 | 
					    if (luckyUser == null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Console.WriteLine("Unable to determine today's lucky user!");
 | 
					        logger.LogError("Unable to determine today's lucky user!");
 | 
				
			||||||
        await dci.DisposeAsync();
 | 
					        await dci.DisposeAsync();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -145,7 +146,6 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    await dci.DisposeAsync();
 | 
					    await dci.DisposeAsync();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
pickOfTheDayTimer.Start();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var startUserAssociationProcess = async (User userToAssociate) =>
 | 
					var startUserAssociationProcess = async (User userToAssociate) =>
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -158,14 +158,14 @@ var startUserAssociationProcess = async (User userToAssociate) =>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Console.WriteLine("Setting up LdapAssociation timer");
 | 
					logger.LogTrace("Setting up LdapAssociation timer");
 | 
				
			||||||
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
					var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
				
			||||||
ldapAssociationTimer.OnOccurence += async (s, ea) =>
 | 
					ldapAssociationTimer.OnOccurence += async (s, ea) =>
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    var dci = DataContext.Instance;
 | 
					    var dci = DataContext.Instance;
 | 
				
			||||||
    if (dci.Users == null)
 | 
					    if (dci.Users == null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Console.WriteLine("Unable to properly initialize DB context!");
 | 
					        logger.LogError("Unable to properly initialize DB context!");
 | 
				
			||||||
        await dci.DisposeAsync();
 | 
					        await dci.DisposeAsync();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -186,13 +186,6 @@ ldapAssociationTimer.OnOccurence += async (s, ea) =>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    await dci.DisposeAsync();
 | 
					    await dci.DisposeAsync();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
ldapAssociationTimer.Start();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var searchResults = LdapIntegration.Instance.SearchInAD(
 | 
					 | 
				
			||||||
    AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
 | 
					 | 
				
			||||||
    $"(memberOf={AppConfiguration.Instance.LDAPConfig.CrewGroup})",
 | 
					 | 
				
			||||||
    SearchScope.Subtree
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Add services to the container.
 | 
					// Add services to the container.
 | 
				
			||||||
builder.Services.AddRazorPages();
 | 
					builder.Services.AddRazorPages();
 | 
				
			||||||
@@ -205,9 +198,20 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
builder.Services.AddSingleton<LdapAuthenticationService>();
 | 
					builder.Services.AddSingleton<LdapAuthenticationService>();
 | 
				
			||||||
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
 | 
					builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
 | 
				
			||||||
 | 
					builder.Services.AddSingleton<SongResolver>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var app = builder.Build();
 | 
					var app = builder.Build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// only start interaction timers in production builds
 | 
				
			||||||
 | 
					// for local/development testing we want those disabled
 | 
				
			||||||
 | 
					if (!app.Environment.IsDevelopment())
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    userCheckTimer.Start();
 | 
				
			||||||
 | 
					    userIntroTimer.Start();
 | 
				
			||||||
 | 
					    pickOfTheDayTimer.Start();
 | 
				
			||||||
 | 
					    ldapAssociationTimer.Start();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Configure the HTTP request pipeline.
 | 
					// Configure the HTTP request pipeline.
 | 
				
			||||||
if (!app.Environment.IsDevelopment())
 | 
					if (!app.Environment.IsDevelopment())
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								song_of_the_day/SongValidators/ISongValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								song_of_the_day/SongValidators/ISongValidator.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					public interface ISongValidator
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    Task<Song> ValidateAsync(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool CanValidateUri(Uri songUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Task<bool> CanExtractSongMetadataAsync(Uri songUri);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								song_of_the_day/SongValidators/SongResolver.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								song_of_the_day/SongValidators/SongResolver.cs
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								song_of_the_day/SongValidators/SongValidatorBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								song_of_the_day/SongValidators/SongValidatorBase.cs
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										40
									
								
								song_of_the_day/SongValidators/SpotifyValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								song_of_the_day/SongValidators/SpotifyValidator.cs
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								song_of_the_day/SongValidators/YoutubeMusicValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								song_of_the_day/SongValidators/YoutubeMusicValidator.cs
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								song_of_the_day/SongValidators/YoutubeValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								song_of_the_day/SongValidators/YoutubeValidator.cs
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										44
									
								
								song_of_the_day/SpotifyIntegration/SpotifyClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								song_of_the_day/SpotifyIntegration/SpotifyClient.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					using SpotifyAPI.Web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class SpotifyApiClient
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private SpotifyClient _spotifyClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public SpotifyApiClient()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var config = SpotifyClientConfig.CreateDefault()
 | 
				
			||||||
 | 
					            .WithAuthenticator(new ClientCredentialsAuthenticator(
 | 
				
			||||||
 | 
					                AppConfiguration.Instance.SpotifyClientId, 
 | 
				
			||||||
 | 
					                AppConfiguration.Instance.SpotifyClientSecret));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _spotifyClient = new SpotifyClient(config);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async Task<List<FullTrack>> GetTrackCandidatesAsync(string trackName, string artistName)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var searchResponse = await _spotifyClient.Search.Item(new SearchRequest(SearchRequest.Types.Track, $"{trackName} {artistName}")
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Limit = 5
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return searchResponse.Tracks.Items ?? new List<FullTrack>();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (APIException ex)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            throw new Exception($"Error fetching tracks by query: \"{trackName} {artistName}\": {ex.Message}", ex);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async Task<FullTrack> GetTrackByIdAsync(string trackId)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return await _spotifyClient.Tracks.Get(trackId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (APIException ex)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            throw new Exception($"Error fetching track by ID: {trackId}: {ex.Message}", ex);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,16 +6,18 @@
 | 
				
			|||||||
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
 | 
					    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <PackageReference Include="AngleSharp" Version="1.3.0" />
 | 
				
			||||||
    <PackageReference Include="CommandLineParser" Version="2.9.1" />
 | 
					    <PackageReference Include="CommandLineParser" Version="2.9.1" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
 | 
					    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
 | 
					    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
 | 
				
			||||||
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
					      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
				
			||||||
      <PrivateAssets>all</PrivateAssets>
 | 
					      <PrivateAssets>all</PrivateAssets>
 | 
				
			||||||
    </PackageReference>
 | 
					    </PackageReference>
 | 
				
			||||||
    <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
 | 
					    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
 | 
				
			||||||
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
 | 
					    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
 | 
				
			||||||
    <PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5" />
 | 
					    <PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5" />
 | 
				
			||||||
    <PackageReference Include="Scalar.AspNetCore" Version="2.1.*" />
 | 
					    <PackageReference Include="Scalar.AspNetCore" Version="2.1.*" />
 | 
				
			||||||
 | 
					    <PackageReference Include="SpotifyAPI.Web" Version="7.2.1" />
 | 
				
			||||||
    <PackageReference Include="System.DirectoryServices.Protocols" Version="*" />
 | 
					    <PackageReference Include="System.DirectoryServices.Protocols" Version="*" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user