feat: implement song submission support, refs #5
This commit is contained in:
		
							
								
								
									
										170
									
								
								song_of_the_day/Data/Migrations/20250601144913_some more model updates.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								song_of_the_day/Data/Migrations/20250601144913_some more model updates.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(DataContext))]
 | 
			
		||||
    [Migration("20250601144913_some more model updates")]
 | 
			
		||||
    partial class somemoremodelupdates
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.3")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("Song", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("SongId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Artist")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("Provider")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SpotifyId")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Url")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Songs");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("Date")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasUsedSuggestion")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("SongId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("SuggestionHelperId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("UserHasSubmitted")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("UserId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SuggestionHelperId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SongSuggestions");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SuggestionHelper", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Title")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SuggestionHelpers");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("User", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("UserId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("AssociationInProgress")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsIntroduced")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("LdapUserName")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("NickName")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SignalMemberId")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("WasChosenForSuggestionThisRound")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Users");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("Song", "Song")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("SuggestionHelper", "SuggestionHelper")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SuggestionHelperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired();
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("User", "User")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Song");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("SuggestionHelper");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("User");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class somemoremodelupdates : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AddColumn<int>(
 | 
			
		||||
                name: "Provider",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<string>(
 | 
			
		||||
                name: "SpotifyId",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "Provider",
 | 
			
		||||
                table: "Songs");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "SpotifyId",
 | 
			
		||||
                table: "Songs");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -35,6 +35,12 @@ namespace song_of_the_day.DataMigrations
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("Provider")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SpotifyId")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Url")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
 | 
			
		||||
using song_of_the_day;
 | 
			
		||||
 | 
			
		||||
public class LinkPreviewAttachment
 | 
			
		||||
{
 | 
			
		||||
    public string Url { get; set; }
 | 
			
		||||
    public string Title { get; set; }
 | 
			
		||||
    public string Description { get; set; }
 | 
			
		||||
    public string Base64Image { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class SignalIntegration
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<SignalIntegration> logger;
 | 
			
		||||
@@ -15,7 +23,8 @@ public class SignalIntegration
 | 
			
		||||
 | 
			
		||||
        var http = new HttpClient()
 | 
			
		||||
        {
 | 
			
		||||
            BaseAddress = new Uri(uri + ":" + port)
 | 
			
		||||
            BaseAddress = new Uri(uri + ":" + port),
 | 
			
		||||
            Timeout = TimeSpan.FromSeconds(180),
 | 
			
		||||
        };
 | 
			
		||||
        apiClient = new swaggerClient(http);
 | 
			
		||||
        apiClient.BaseUrl = "";
 | 
			
		||||
@@ -26,6 +35,12 @@ public class SignalIntegration
 | 
			
		||||
 | 
			
		||||
    private string phoneNumber;
 | 
			
		||||
 | 
			
		||||
    public async Task GetMessagesAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var messages = await apiClient.ReceiveAsync(this.phoneNumber, "120", "true", "true", "50", "false");
 | 
			
		||||
        logger.LogInformation($"Received {messages.Count} Signal messages.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ListGroupsAsync()
 | 
			
		||||
    {
 | 
			
		||||
        logger.LogDebug("Listing all groups for phone number: {PhoneNumber}", this.phoneNumber);
 | 
			
		||||
@@ -51,7 +66,7 @@ public class SignalIntegration
 | 
			
		||||
            SendMessageV2 data = new SendMessageV2();
 | 
			
		||||
            data.Recipients = new List<string>();
 | 
			
		||||
            data.Recipients.Add(AppConfiguration.Instance.SignalGroupId);
 | 
			
		||||
            data.Message = message;
 | 
			
		||||
            data.Message = (AppConfiguration.Instance.UseBotTag ? "**[Proggy]**\n" : "") + message;
 | 
			
		||||
            data.Text_mode = SendMessageV2Text_mode.Styled;
 | 
			
		||||
            data.Number = AppConfiguration.Instance.HostPhoneNumber;
 | 
			
		||||
            var response = await apiClient.Send2Async(data);
 | 
			
		||||
@@ -62,6 +77,29 @@ public class SignalIntegration
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SendMessageToGroupAsync(string message, LinkPreviewAttachment previewData)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            SendMessageV2 data = new SendMessageV2();
 | 
			
		||||
            data.Recipients = new List<string>();
 | 
			
		||||
            data.Recipients.Add(AppConfiguration.Instance.SignalGroupId);
 | 
			
		||||
            data.Message = (AppConfiguration.Instance.UseBotTag ? "**[Proggy]**\n" : "") + message;
 | 
			
		||||
            data.Text_mode = SendMessageV2Text_mode.Styled;
 | 
			
		||||
            data.Number = AppConfiguration.Instance.HostPhoneNumber;
 | 
			
		||||
            data.Link_preview = new LinkPreviewType();
 | 
			
		||||
            data.Link_preview.Url = previewData.Url;
 | 
			
		||||
            data.Link_preview.Title = previewData.Title;
 | 
			
		||||
            data.Link_preview.Description = previewData.Description;
 | 
			
		||||
            data.Link_preview.Base64_thumbnail = previewData.Base64Image;
 | 
			
		||||
            var response = await apiClient.Send2Async(data);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError("Exception (SendMessageToGroupAsync): " + ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SendMessageToUserAsync(string message, string userId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
@@ -80,6 +118,29 @@ public class SignalIntegration
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SendMessageToUserAsync(string message, LinkPreviewAttachment previewData, string userId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            SendMessageV2 data = new SendMessageV2();
 | 
			
		||||
            data.Recipients = new List<string>();
 | 
			
		||||
            data.Recipients.Add(userId);
 | 
			
		||||
            data.Message = (AppConfiguration.Instance.UseBotTag ? "**[Proggy]**\n" : "") + message;
 | 
			
		||||
            data.Text_mode = SendMessageV2Text_mode.Styled;
 | 
			
		||||
            data.Number = AppConfiguration.Instance.HostPhoneNumber;
 | 
			
		||||
            data.Link_preview = new LinkPreviewType();
 | 
			
		||||
            data.Link_preview.Url = previewData.Url;
 | 
			
		||||
            data.Link_preview.Title = previewData.Title;
 | 
			
		||||
            data.Link_preview.Description = previewData.Description;
 | 
			
		||||
            data.Link_preview.Base64_thumbnail = previewData.Base64Image;
 | 
			
		||||
            var response = await apiClient.Send2Async(data);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError("Exception (SendMessageToUserAsync): " + ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task IntroduceUserAsync(User user)
 | 
			
		||||
    {
 | 
			
		||||
        if (user == null || user.SignalMemberId == null)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,29 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
    <h1 class="display-4">Welcome</h1>
 | 
			
		||||
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
 | 
			
		||||
    <h1 class="display-4">Submission History</h1>
 | 
			
		||||
    <table>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th>Date</th>
 | 
			
		||||
            <th>Song</th>
 | 
			
		||||
            <th>Submitter</th>
 | 
			
		||||
            <th>Details</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
        @foreach(var songSuggestion in Model.SongSuggestions)
 | 
			
		||||
        {
 | 
			
		||||
            @if(songSuggestion != null && songSuggestion.Song != null && songSuggestion.User != null && songSuggestion.UserHasSubmitted)
 | 
			
		||||
            {
 | 
			
		||||
                var displayName = string.IsNullOrEmpty(songSuggestion?.User?.NickName) 
 | 
			
		||||
                    ? songSuggestion?.User.Name 
 | 
			
		||||
                    : songSuggestion.User.NickName;
 | 
			
		||||
                    
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>@songSuggestion?.Date.ToString("dd. MM. yyyy")</td>
 | 
			
		||||
                    <td><a href="@songSuggestion?.Song.Url" target="_blank">@string.Format("{0} - {1}", songSuggestion?.Song.Name, songSuggestion?.Song.Artist)</a></td>
 | 
			
		||||
                    <td>@displayName</td>
 | 
			
		||||
                    <td><a href="/SongSubmission/@songSuggestion?.Id">View</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    </table>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.RazorPages;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace sotd.Pages;
 | 
			
		||||
 | 
			
		||||
@@ -12,8 +14,16 @@ public class IndexModel : PageModel
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void OnGet()
 | 
			
		||||
    {
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public List<SongSuggestion> SongSuggestions { get; set; } = new List<SongSuggestion>();
 | 
			
		||||
 | 
			
		||||
    public async Task OnGet()
 | 
			
		||||
    {
 | 
			
		||||
        using var dci = DataContext.Instance;
 | 
			
		||||
        this.SongSuggestions = dci.SongSuggestions.OrderByDescending(s => s.Date)
 | 
			
		||||
            .Take(50)
 | 
			
		||||
            .Include(s => s.Song)
 | 
			
		||||
            .Include(s => s.User)
 | 
			
		||||
            .ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								song_of_the_day/Pages/Shared/SongPartialModel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								song_of_the_day/Pages/Shared/SongPartialModel.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
public class SongPartialModel
 | 
			
		||||
{
 | 
			
		||||
    public Song InnerSong { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string? Artist => InnerSong.Artist;
 | 
			
		||||
 | 
			
		||||
    public string? Name => InnerSong.Name;
 | 
			
		||||
 | 
			
		||||
    public string? SpotifyId => InnerSong.SpotifyId;
 | 
			
		||||
 | 
			
		||||
    public string? Url => InnerSong.Url;
 | 
			
		||||
 | 
			
		||||
    public SongProvider? Provider => InnerSong.Provider;
 | 
			
		||||
 | 
			
		||||
    public bool IsPageReadonly { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
                            this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
 | 
			
		||||
                        {
 | 
			
		||||
                            <li class="nav-item">
 | 
			
		||||
                                <a class="nav-link text-dark" asp-area="" asp-page="/SubmitSongs">Submit Songs</a>
 | 
			
		||||
                                <a class="nav-link text-dark" href="/SongSubmission/">Song Submissions</a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        }
 | 
			
		||||
                    </ul>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,4 +45,4 @@ button.accept-policy {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  line-height: 60px;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
@model Song
 | 
			
		||||
@model SongPartialModel
 | 
			
		||||
<label asp-for="Name">Name:</label>
 | 
			
		||||
<input asp-for="Name" />
 | 
			
		||||
<input asp-for="Name" oninput="UpdateSongSuggestions()" disabled="@Model.IsPageReadonly" />
 | 
			
		||||
<label asp-for="Artist">Artist:</label>
 | 
			
		||||
<input asp-for="Artist" />
 | 
			
		||||
<input asp-for="Artist" oninput="UpdateSongSuggestions()" disabled="@Model.IsPageReadonly" />
 | 
			
		||||
<label asp-for="SpotifyId">Spotify ID:</label>
 | 
			
		||||
<input asp-for="SpotifyId" />
 | 
			
		||||
<input asp-for="SpotifyId" readonly="@Model.Provider.Equals(SongProvider.Spotify)" disabled="@Model.IsPageReadonly" />
 | 
			
		||||
<input asp-for="Provider" hidden />
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
@model List<SpotifyAPI.Web.FullTrack>
 | 
			
		||||
<div class="spotifySongSelector">
 | 
			
		||||
    @foreach(var track in Model)
 | 
			
		||||
    {
 | 
			
		||||
        <div class="songSelectorButton" onclick="clickHandler(this)">
 | 
			
		||||
            <div class="songName">@track.Name</div>
 | 
			
		||||
            <div class="artist">@track.Artists[0].Name</div>
 | 
			
		||||
            <div class="album">@track.Album.Name</div>
 | 
			
		||||
            <div class="id" hidden>@track.Id</div>
 | 
			
		||||
            <div class="albumCover">
 | 
			
		||||
                <img src="@track.Album.Images[0].Url" width="50px" height="50px"/>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    }
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    function clickHandler(t) {
 | 
			
		||||
        // remove previous selection
 | 
			
		||||
        $('.songSelectorButton').removeClass("selected");
 | 
			
		||||
        t.classList.add("selected");
 | 
			
		||||
        var idElement = t.getElementsByClassName("id")[0]
 | 
			
		||||
        console.log(idElement);
 | 
			
		||||
        SetSpotifyId(idElement.textContent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function initializeSelection() {
 | 
			
		||||
        var currentSpotifyId = GetSpotifyId();
 | 
			
		||||
        $('.songSelectorButton').each(function(i, e) {
 | 
			
		||||
            if(e.getElementsByClassName("id")[0].textContent == currentSpotifyId)
 | 
			
		||||
            {
 | 
			
		||||
                e.classList.add("selected");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initializeSelection();
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
.songSelectorButton {
 | 
			
		||||
  background-color: #e3e6e7;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.songSelectorButton:nth-child(2n) {
 | 
			
		||||
  background-color: #cacbce;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.songSelectorButton.selected {
 | 
			
		||||
  background-color: #a0c3e5;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  border-width: 2px;
 | 
			
		||||
  border-color: #285786;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										158
									
								
								song_of_the_day/Pages/SongSubmission.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								song_of_the_day/Pages/SongSubmission.cshtml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
			
		||||
@page "{submissionIndex:int?}"
 | 
			
		||||
@model SongSubmissionModel
 | 
			
		||||
@{
 | 
			
		||||
    ViewData["Title"] = this.Model.SubmissionIndex == null ? "Song Submissions" : "New Song Submission";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@if(Model.SubmissionIndex == null)
 | 
			
		||||
{
 | 
			
		||||
    <div class="text-left">
 | 
			
		||||
        <h1>Your Submission History:</h1>
 | 
			
		||||
        <table id="submissionTable">
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>Date</th>
 | 
			
		||||
                <th>Suggestion</th>
 | 
			
		||||
                <th>Song</th>
 | 
			
		||||
                <th></th>
 | 
			
		||||
            </tr>
 | 
			
		||||
            @foreach(var submission in Model.UserSongSubmissions)
 | 
			
		||||
            {
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td class="submissionDate">
 | 
			
		||||
                        @submission.Date.ToString("dd. MM. yyyy")
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="submissionSuggestion">
 | 
			
		||||
                        @if(submission.Song == null || submission.HasUsedSuggestion)
 | 
			
		||||
                        {
 | 
			
		||||
                            @submission.SuggestionHelper.Title
 | 
			
		||||
                        }
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="submissionSong">
 | 
			
		||||
                        @if(submission.Song != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            @string.Format("{0} - {1}", submission.Song.Name, submission.Song.Artist);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            <div style="font-style: italic;">No submission yet!</div>
 | 
			
		||||
                        }
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="=viewLink">
 | 
			
		||||
                        @{
 | 
			
		||||
                            var buttonClass = submission.Song == null ? "submitButton" : "viewButton";
 | 
			
		||||
                            var buttonText = submission.Song == null ? "Submit Song" : "View";
 | 
			
		||||
                        }
 | 
			
		||||
                        <button class=@buttonClass onclick="redirectTo(@submission.Id)">@buttonText</button>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            }
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
}
 | 
			
		||||
else
 | 
			
		||||
{
 | 
			
		||||
    <div class="text-left">
 | 
			
		||||
        <div class="suggestionHelper">
 | 
			
		||||
            <div class="title">Today's suggestionHelper is: <b>@Model.SuggestionHelper.Title</b></div>
 | 
			
		||||
            <div class="description" style="font-style: italic;">@Model.SuggestionHelper.Description</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <form method="post">
 | 
			
		||||
            <label asp-for="SubmitUrl" >Song Url:</label>
 | 
			
		||||
            <input asp-for="SubmitUrl" oninput="Update(this)"  disabled="@Model.IsPageReadonly" />
 | 
			
		||||
            <label asp-for="HasUsedSuggestion">Submission has used suggestion.</label>
 | 
			
		||||
            <input asp-for="HasUsedSuggestion" checked  disabled="@Model.IsPageReadonly" />
 | 
			
		||||
            @{
 | 
			
		||||
                var songPartialModel = new SongPartialModel() {
 | 
			
		||||
                    InnerSong = Model.SongData,
 | 
			
		||||
                    IsPageReadonly = Model.IsPageReadonly
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            <div id="songdata">
 | 
			
		||||
                <partial name="_SongPartial" model="@songPartialModel" />
 | 
			
		||||
            </div>
 | 
			
		||||
            @if(!Model.IsPageReadonly)
 | 
			
		||||
            {
 | 
			
		||||
                <div id="suggestionPicker">
 | 
			
		||||
                    <partial name="_SpotifySongSuggestionsPartial" model="@Model.SpotifySuggestions" />
 | 
			
		||||
                </div>
 | 
			
		||||
            }
 | 
			
		||||
            <input type="submit" id="songsubmit" title="Submit" value="Submit" disabled="@Model.CanSubmit" />
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@if(Model.SubmissionIndex == null)
 | 
			
		||||
{
 | 
			
		||||
    // inject relevant scripts
 | 
			
		||||
    <script>
 | 
			
		||||
        function redirectTo(id) {
 | 
			
		||||
            window.location.href = '/SongSubmission/' + id;
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
}
 | 
			
		||||
else
 | 
			
		||||
{
 | 
			
		||||
    <script>
 | 
			
		||||
        var skipUpdate = false;
 | 
			
		||||
 | 
			
		||||
        function ReevaluateSubmit(callback) {
 | 
			
		||||
            var canSubmit = $('#songdata #Name').val().length > 0 && $('#songdata #Artist').val().length > 0;
 | 
			
		||||
            if(canSubmit)
 | 
			
		||||
            {
 | 
			
		||||
                $('#songsubmit').removeAttr("disabled");
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                $('#songsubmit').attr("disabled", "disabled");
 | 
			
		||||
            }
 | 
			
		||||
            if(callback)
 | 
			
		||||
            {
 | 
			
		||||
                callback();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function Update(t) {
 | 
			
		||||
            var songDataUrl = '?handler=Update&&SubmitUrl=' + $(t).val();
 | 
			
		||||
            $('#songdata').load(songDataUrl, null, function() {
 | 
			
		||||
                ReevaluateSubmit(function() {
 | 
			
		||||
                    UpdateSongSuggestions(t);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function UpdateSongSuggestions() {
 | 
			
		||||
            if(skipUpdate)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }  
 | 
			
		||||
            skipUpdate = true;
 | 
			
		||||
            var trackName = $('#songdata #Name').val();
 | 
			
		||||
            var artistName = $('#songdata #Artist').val();
 | 
			
		||||
            var submitUrl = $('#SubmitUrl').val();
 | 
			
		||||
            var provider = $('#songdata #Provider').val();
 | 
			
		||||
            var spotifyId = $('#songdata #SpotifyId').val();
 | 
			
		||||
            var suggestionDataUrl = '?handler=SpotifySuggestions&song=' + encodeURIComponent(trackName) 
 | 
			
		||||
                + '&artist=' + encodeURIComponent(artistName)
 | 
			
		||||
                + '&submitUrl=' + encodeURI(submitUrl)
 | 
			
		||||
                + '&provider=' + encodeURI(provider)
 | 
			
		||||
                + '&spotifyId=' + encodeURI(spotifyId);
 | 
			
		||||
            $('#suggestionPicker').load(suggestionDataUrl, null, function() {
 | 
			
		||||
                ReevaluateSubmit();
 | 
			
		||||
                skipUpdate = false;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function SetSpotifyId(id) {
 | 
			
		||||
            $('#SpotifyId').val(id);
 | 
			
		||||
            ReevaluateSubmit();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function GetSpotifyId() {
 | 
			
		||||
            return $('#SpotifyId').val();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        document.getElementById('songdata').oninput = ReevaluateSubmit;
 | 
			
		||||
        ReevaluateSubmit();
 | 
			
		||||
    </script>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										237
									
								
								song_of_the_day/Pages/SongSubmission.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								song_of_the_day/Pages/SongSubmission.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.Http.HttpResults;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.RazorPages;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.VisualBasic;
 | 
			
		||||
 | 
			
		||||
namespace sotd.Pages;
 | 
			
		||||
 | 
			
		||||
public class SongSubmissionModel : PageModel
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<UserModel> _logger;
 | 
			
		||||
 | 
			
		||||
    private SongResolver songResolver;
 | 
			
		||||
 | 
			
		||||
    private string _submitUrl;
 | 
			
		||||
 | 
			
		||||
    private SpotifyApiClient spotifyApiClient;
 | 
			
		||||
 | 
			
		||||
    private SignalIntegration signalIntegration;
 | 
			
		||||
 | 
			
		||||
    public SongSubmissionModel(ILogger<UserModel> logger, SongResolver songResolver, SpotifyApiClient spotifyApiClient, SignalIntegration signalIntegration)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        this.spotifyApiClient = spotifyApiClient;
 | 
			
		||||
        this.songResolver = songResolver;
 | 
			
		||||
        this.signalIntegration = signalIntegration;
 | 
			
		||||
        _submitUrl = string.Empty;
 | 
			
		||||
        SongData = new Song();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public int? SubmissionIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public bool IsValidUrl { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public bool IsPageReadonly { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public SuggestionHelper SuggestionHelper { get; set; } = null;
 | 
			
		||||
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public List<SongSuggestion> UserSongSubmissions { get; set; } = [];
 | 
			
		||||
 | 
			
		||||
    [BindProperty(SupportsGet = true)]
 | 
			
		||||
    public string SubmitUrl
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            return _submitUrl;
 | 
			
		||||
        }
 | 
			
		||||
        set
 | 
			
		||||
        {
 | 
			
		||||
            _submitUrl = value == null ? string.Empty : 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;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public List<SpotifyAPI.Web.FullTrack> SpotifySuggestions
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            var suggestionList = new List<SpotifyAPI.Web.FullTrack>();
 | 
			
		||||
            if (!string.IsNullOrEmpty(this.SongData.Name) && !string.IsNullOrEmpty(this.SongData.Artist))
 | 
			
		||||
            {
 | 
			
		||||
                suggestionList.AddRange(spotifyApiClient.GetTrackCandidatesAsync(this.SongData.Name, this.SongData.Artist).Result);
 | 
			
		||||
            }
 | 
			
		||||
            return suggestionList;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IActionResult OnGetSpotifySuggestions(string song, string artist, string submitUrl, SongProvider provider, string spotifyId)
 | 
			
		||||
    {
 | 
			
		||||
        this.SongData.Name = song;
 | 
			
		||||
        this.SongData.Artist = artist;
 | 
			
		||||
        this.SongData.Url = submitUrl;
 | 
			
		||||
        this.SongData.Provider = provider;
 | 
			
		||||
        this.SongData.SpotifyId = spotifyId;
 | 
			
		||||
        return Partial("_SpotifySongSuggestionsPartial", this.SpotifySuggestions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public string CanSubmit
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            var disableCondition = !(string.IsNullOrEmpty(SongData?.Artist) && string.IsNullOrEmpty(SongData?.Name));
 | 
			
		||||
            return disableCondition ? "" : "disabled";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [BindProperty(SupportsGet = true)]
 | 
			
		||||
    public Song SongData { get; set; }
 | 
			
		||||
 | 
			
		||||
    [BindProperty(SupportsGet = true)]
 | 
			
		||||
    public bool HasUsedSuggestion { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    private async Task UpdateGroup(SongSuggestion suggestion)
 | 
			
		||||
    {
 | 
			
		||||
        var isItToday = suggestion.Date.Date == DateTime.Today;
 | 
			
		||||
        var dateString = isItToday ? "submission for today" : "submission from " + suggestion.Date.ToString("dd. MM. yyyy");
 | 
			
		||||
        var displayName = string.IsNullOrEmpty(suggestion.User.NickName) ? suggestion.User.Name : suggestion.User.NickName;
 | 
			
		||||
        var imageBuilder = await songResolver.GetPreviewImageAsync(suggestion.Song.SpotifyId);
 | 
			
		||||
        var previewData = new LinkPreviewAttachment()
 | 
			
		||||
        {
 | 
			
		||||
            Title = suggestion.Song.Name,
 | 
			
		||||
            Description = suggestion.Song.Artist,
 | 
			
		||||
            Url = suggestion.Song.Url,
 | 
			
		||||
            Base64Image = imageBuilder.ToString(),
 | 
			
		||||
        };
 | 
			
		||||
        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}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IActionResult> OnPost(int? submissionIndex)
 | 
			
		||||
    {
 | 
			
		||||
        if (submissionIndex == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception("Attempt to submit song without submissionId");
 | 
			
		||||
        }
 | 
			
		||||
        // Todo implement save submission
 | 
			
		||||
        using var dci = DataContext.Instance;
 | 
			
		||||
        var suggestion = dci.SongSuggestions
 | 
			
		||||
                .Include(s => s.User)
 | 
			
		||||
                .Include(s => s.SuggestionHelper)
 | 
			
		||||
                .Where(s => s.Id == submissionIndex)
 | 
			
		||||
                .FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        if (suggestion == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception("Attempt to submit song with invalid submissionId");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.SongData.Artist = Request.Form["Artist"];
 | 
			
		||||
        this.SongData.Url = Request.Form["SubmitUrl"];
 | 
			
		||||
        this.SongData.Name = Request.Form["Name"];
 | 
			
		||||
        this.SongData.SpotifyId = Request.Form["SpotifyId"];
 | 
			
		||||
        this.SongData.Provider = Enum.Parse<SongProvider>(Request.Form["Provider"]);
 | 
			
		||||
 | 
			
		||||
        // Let's check if we already have this song in the DB
 | 
			
		||||
        var songToUse = dci.Songs.Where(s => s.SpotifyId == this.SongData.SpotifyId).FirstOrDefault();
 | 
			
		||||
        if (songToUse == null)
 | 
			
		||||
        {
 | 
			
		||||
            // Song was not in DB yet, creating a new one
 | 
			
		||||
            var newSong = new Song()
 | 
			
		||||
            {
 | 
			
		||||
                SpotifyId = this.SongData.SpotifyId,
 | 
			
		||||
                Artist = this.SongData.Artist,
 | 
			
		||||
                Name = this.SongData.Name,
 | 
			
		||||
                Url = this.SongData.Url,
 | 
			
		||||
                Provider = this.SongData.Provider,
 | 
			
		||||
            };
 | 
			
		||||
            var dbRecord = dci.Songs.Add(newSong);
 | 
			
		||||
            var newId = await dci.SaveChangesAsync();
 | 
			
		||||
            songToUse = dbRecord.Entity;
 | 
			
		||||
        }
 | 
			
		||||
        suggestion.Song = songToUse;
 | 
			
		||||
        suggestion.UserHasSubmitted = true;
 | 
			
		||||
        suggestion.HasUsedSuggestion = this.HasUsedSuggestion;
 | 
			
		||||
        dci.Update(suggestion);
 | 
			
		||||
        await dci.SaveChangesAsync();
 | 
			
		||||
        // load overview page again after submitting
 | 
			
		||||
 | 
			
		||||
        await UpdateGroup(suggestion);
 | 
			
		||||
 | 
			
		||||
        return RedirectToPage("SongSubmission");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void OnGet(int? submissionIndex)
 | 
			
		||||
    {
 | 
			
		||||
        this.SubmissionIndex = submissionIndex;
 | 
			
		||||
 | 
			
		||||
        // we want to show overview, so we need to fetch user submission list
 | 
			
		||||
        if (submissionIndex == null)
 | 
			
		||||
        {
 | 
			
		||||
            using var dci = DataContext.Instance;
 | 
			
		||||
            var currentUserName = this.User.Identity.Name;
 | 
			
		||||
            this.UserSongSubmissions = dci.SongSuggestions
 | 
			
		||||
                            .Include(s => s.SuggestionHelper)
 | 
			
		||||
                            .Include(s => s.Song)
 | 
			
		||||
                            .Where(s => s.User.LdapUserName.Equals(currentUserName))
 | 
			
		||||
                            .OrderByDescending(s => s.Date)
 | 
			
		||||
                            .ToList();
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            using var dci = DataContext.Instance;
 | 
			
		||||
            var songSuggestion = dci.SongSuggestions
 | 
			
		||||
                            .Include(s => s.SuggestionHelper)
 | 
			
		||||
                            .Include(s => s.Song)
 | 
			
		||||
                            .Where(s => s.Id.Equals(submissionIndex.Value))
 | 
			
		||||
                            .First();
 | 
			
		||||
            this.SuggestionHelper = songSuggestion.SuggestionHelper;
 | 
			
		||||
            this.SongData = songSuggestion.Song == null ? new Song() : songSuggestion.Song;
 | 
			
		||||
            this.SubmitUrl = this.SongData.Url;
 | 
			
		||||
            if (!string.IsNullOrEmpty(this.SongData.Name) && !string.IsNullOrEmpty(this.SongData.Artist))
 | 
			
		||||
            {
 | 
			
		||||
                this.IsPageReadonly = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IActionResult OnGetUpdate()
 | 
			
		||||
    {
 | 
			
		||||
        var songUrl = Request.Query["SubmitUrl"];
 | 
			
		||||
        this.SubmitUrl = songUrl.ToString();
 | 
			
		||||
        var songPartialModel = new SongPartialModel()
 | 
			
		||||
        {
 | 
			
		||||
            InnerSong = SongData,
 | 
			
		||||
            IsPageReadonly = false
 | 
			
		||||
        };
 | 
			
		||||
        return Partial("_SongPartial", songPartialModel); ;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
@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>
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
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;
 | 
			
		||||
        _submitUrl = string.Empty;
 | 
			
		||||
        SongData = new Song();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [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); ;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,6 +20,7 @@ builder.Services.AddSingleton<LdapAuthenticationService>();
 | 
			
		||||
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
 | 
			
		||||
builder.Services.AddSingleton<SignalIntegration>();
 | 
			
		||||
builder.Services.AddSingleton<LdapIntegration>();
 | 
			
		||||
builder.Services.AddSingleton<SpotifyApiClient>();
 | 
			
		||||
builder.Services.AddSingleton<SongResolver>();
 | 
			
		||||
 | 
			
		||||
var app = builder.Build();
 | 
			
		||||
@@ -143,15 +144,15 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
 | 
			
		||||
    };
 | 
			
		||||
    if (luckyUser.SignalMemberId is string signalId)
 | 
			
		||||
    {
 | 
			
		||||
        await dci.SongSuggestions.AddAsync(newSongSuggestion);
 | 
			
		||||
        var result = await dci.SongSuggestions.AddAsync(newSongSuggestion);
 | 
			
		||||
        newSongSuggestion = result.Entity;
 | 
			
		||||
        luckyUser.WasChosenForSuggestionThisRound = true;
 | 
			
		||||
        await dci.SaveChangesAsync();
 | 
			
		||||
        var signalIntegration = app.Services.GetService<SignalIntegration>();
 | 
			
		||||
        await signalIntegration.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
 | 
			
		||||
        await signalIntegration.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*");
 | 
			
		||||
        await signalIntegration.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId);
 | 
			
		||||
        await signalIntegration.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId);
 | 
			
		||||
        await signalIntegration.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId);
 | 
			
		||||
        await signalIntegration.SendMessageToUserAsync($"Please navigate to https://sord.disi.dev/SongSubmission/{newSongSuggestion.Id} to submit your choice!", luckyUser.SignalMemberId);
 | 
			
		||||
    }
 | 
			
		||||
    await dci.DisposeAsync();
 | 
			
		||||
};
 | 
			
		||||
@@ -197,6 +198,16 @@ ldapAssociationTimer.OnOccurence += async (s, ea) =>
 | 
			
		||||
    await dci.DisposeAsync();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
logger.LogTrace("Setting up MessageSync timer");
 | 
			
		||||
var messageSyncTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
 | 
			
		||||
messageSyncTimer.OnOccurence += async (s, ea) =>
 | 
			
		||||
{
 | 
			
		||||
    var signalIntegration = app.Services.GetService<SignalIntegration>();
 | 
			
		||||
    await signalIntegration.GetMessagesAsync();
 | 
			
		||||
};
 | 
			
		||||
// disabled for now, is still buggy
 | 
			
		||||
// messageSyncTimer.Start();
 | 
			
		||||
 | 
			
		||||
// only start interaction timers in production builds
 | 
			
		||||
// for local/development testing we want those disabled
 | 
			
		||||
if (!app.Environment.IsDevelopment())
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								song_of_the_day/SongValidators/Base64UrlImageBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								song_of_the_day/SongValidators/Base64UrlImageBuilder.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
public class Base64UrlImageBuilder
 | 
			
		||||
{
 | 
			
		||||
    public string ContentType { set; get; }
 | 
			
		||||
 | 
			
		||||
    public string Url
 | 
			
		||||
    {
 | 
			
		||||
        set
 | 
			
		||||
        {
 | 
			
		||||
            var httpClient = new HttpClient();
 | 
			
		||||
            var response = (httpClient.GetAsync(new Uri($"{value}"))).Result;
 | 
			
		||||
            var bytes = (response.Content.ReadAsByteArrayAsync()).Result;
 | 
			
		||||
            FileContents = Convert.ToBase64String(bytes);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string FileContents { get; set; }
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
    {
 | 
			
		||||
        //return $"data:{ContentType};base64,{FileContents}";
 | 
			
		||||
        return $"{FileContents}";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,4 +5,6 @@ public interface ISongValidator
 | 
			
		||||
    bool CanValidateUri(Uri songUri);
 | 
			
		||||
 | 
			
		||||
    Task<bool> CanExtractSongMetadataAsync(Uri songUri);
 | 
			
		||||
 | 
			
		||||
    SongProvider GetSongProvider();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using SpotifyAPI.Web;
 | 
			
		||||
 | 
			
		||||
public class SongResolver
 | 
			
		||||
{
 | 
			
		||||
@@ -6,16 +7,20 @@ public class SongResolver
 | 
			
		||||
 | 
			
		||||
    private readonly ILogger<SongResolver> logger;
 | 
			
		||||
 | 
			
		||||
    public SongResolver(ILogger<SongResolver> logger)
 | 
			
		||||
    private SpotifyApiClient spotifyApiClient;
 | 
			
		||||
 | 
			
		||||
    public SongResolver(ILogger<SongResolver> logger, ILoggerFactory loggerFactory, SpotifyApiClient spotifyApiClient)
 | 
			
		||||
    {
 | 
			
		||||
        this.logger = logger;
 | 
			
		||||
 | 
			
		||||
        this._songValidators = new List<ISongValidator>();
 | 
			
		||||
        this.spotifyApiClient = spotifyApiClient;
 | 
			
		||||
 | 
			
		||||
        foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
 | 
			
		||||
                 .Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base"))))
 | 
			
		||||
                 .Where(mytype => { return (mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.Split("`")[0].EndsWith("Base"))); }))
 | 
			
		||||
        {
 | 
			
		||||
            if (Activator.CreateInstance(mytype) is ISongValidator validator)
 | 
			
		||||
            var typedLogger = loggerFactory.CreateLogger(mytype);
 | 
			
		||||
            if (Activator.CreateInstance(mytype, typedLogger, spotifyApiClient) is ISongValidator validator)
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name);
 | 
			
		||||
                this._songValidators = this._songValidators.Append(validator);
 | 
			
		||||
@@ -65,4 +70,15 @@ public class SongResolver
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<Base64UrlImageBuilder> GetPreviewImageAsync(string spotifyId)
 | 
			
		||||
    {
 | 
			
		||||
        var track = await spotifyApiClient.GetTrackByIdAsync(spotifyId);
 | 
			
		||||
        var url = track.Album.Images.FirstOrDefault().Url;
 | 
			
		||||
        return new Base64UrlImageBuilder()
 | 
			
		||||
        {
 | 
			
		||||
            Url = url,
 | 
			
		||||
            ContentType = "image/jpeg"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
public abstract class SongValidatorBase : ISongValidator
 | 
			
		||||
{
 | 
			
		||||
@@ -8,9 +9,22 @@ public abstract class SongValidatorBase : ISongValidator
 | 
			
		||||
 | 
			
		||||
    public abstract bool CanValidateUri(Uri songUri);
 | 
			
		||||
 | 
			
		||||
    protected string LookupSpotifyId(string songName, string songArtist)
 | 
			
		||||
    public abstract SongProvider GetSongProvider();
 | 
			
		||||
 | 
			
		||||
    protected SpotifyApiClient _spotifyApiClient;
 | 
			
		||||
 | 
			
		||||
    protected ILogger _logger;
 | 
			
		||||
 | 
			
		||||
    public SongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient)
 | 
			
		||||
    {
 | 
			
		||||
        // TODO: Implement Spotify ID lookup logic
 | 
			
		||||
        return songName + " by " + songArtist;
 | 
			
		||||
        _spotifyApiClient = spotifyApiClient;
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task<string> LookupSpotifyIdAsync(string songName, string songArtist)
 | 
			
		||||
    {
 | 
			
		||||
        var candidates = await _spotifyApiClient.GetTrackCandidatesAsync(songName, songArtist);
 | 
			
		||||
        return candidates.Any() ? candidates[0].Id : "";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,24 +7,25 @@ public class SpotifyValidator : UriBasedSongValidatorBase
 | 
			
		||||
{
 | 
			
		||||
    public override string UriValidatorRegex => @"^(https?://)?open.spotify.com/track/([a-zA-Z0-9_-]{22})(\?si=[a-zA-Z0-9_-]+)?$";
 | 
			
		||||
 | 
			
		||||
    private SpotifyApiClient spotifyApiClient;
 | 
			
		||||
 | 
			
		||||
    public SpotifyValidator()
 | 
			
		||||
    {
 | 
			
		||||
        spotifyApiClient = new SpotifyApiClient();
 | 
			
		||||
    }
 | 
			
		||||
    public SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient)
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
    public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        return Task.FromResult(this.CanValidateUri(songUri));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override SongProvider GetSongProvider()
 | 
			
		||||
    {
 | 
			
		||||
        return SongProvider.Spotify;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<Song> ValidateAsync(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
 | 
			
		||||
        var trackIdMatch = regexp.Match(songUri.ToString()).Groups[2].Value;
 | 
			
		||||
 | 
			
		||||
        var track = await spotifyApiClient.GetTrackByIdAsync(trackIdMatch);
 | 
			
		||||
        var track = await _spotifyApiClient.GetTrackByIdAsync(trackIdMatch);
 | 
			
		||||
 | 
			
		||||
        var song = new Song
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,17 @@ public abstract class UriBasedSongValidatorBase : SongValidatorBase
 | 
			
		||||
{
 | 
			
		||||
    public abstract string UriValidatorRegex { get; }
 | 
			
		||||
 | 
			
		||||
    public override bool CanValidateUri(Uri songUri)
 | 
			
		||||
    public UriBasedSongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
 | 
			
		||||
    {}
 | 
			
		||||
 | 
			
		||||
    public Match GetUriMatch(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
 | 
			
		||||
        return regexp.Match(songUri.ToString()).Success;
 | 
			
		||||
        return regexp.Match(songUri.ToString());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override bool CanValidateUri(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        return GetUriMatch(songUri).Success;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,22 @@
 | 
			
		||||
using AngleSharp;
 | 
			
		||||
using AngleSharp.Dom;
 | 
			
		||||
using YouTubeMusicAPI.Client;
 | 
			
		||||
 | 
			
		||||
public class YoutubeMusicValidator : UriBasedSongValidatorBase
 | 
			
		||||
{
 | 
			
		||||
    public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
 | 
			
		||||
    public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/playlist\?list=)([a-zA-Z0-9_-]+)";
 | 
			
		||||
 | 
			
		||||
    private YouTubeMusicClient youtubeClient;
 | 
			
		||||
 | 
			
		||||
    public YoutubeMusicValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
 | 
			
		||||
    {
 | 
			
		||||
        youtubeClient = new YouTubeMusicClient(logger, "AT", null, null, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override SongProvider GetSongProvider()
 | 
			
		||||
    {
 | 
			
		||||
        return SongProvider.YoutubeMusic;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
@@ -12,35 +25,23 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
 | 
			
		||||
 | 
			
		||||
    public override async Task<Song> ValidateAsync(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        var title = string.Empty;
 | 
			
		||||
        var artist = string.Empty;
 | 
			
		||||
        var match = this.GetUriMatch(songUri);
 | 
			
		||||
        var playlistId = match.Groups[3].Value;
 | 
			
		||||
 | 
			
		||||
        using (HttpClient httpClient = new HttpClient())
 | 
			
		||||
        {
 | 
			
		||||
            var response = await httpClient.GetAsync(songUri);
 | 
			
		||||
            var config = Configuration.Default.WithDefaultLoader();
 | 
			
		||||
            var context = BrowsingContext.New(config);
 | 
			
		||||
            using (var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
 | 
			
		||||
            {
 | 
			
		||||
                // document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("song-title")[0].innerHTML
 | 
			
		||||
                title = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".song-title")?.InnerHtml;
 | 
			
		||||
                // document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("byline")[0].innerHTML
 | 
			
		||||
                artist = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".byline")?.InnerHtml;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var playlistResult = await youtubeClient.GetCommunityPlaylistInfoAsync(playlistId);
 | 
			
		||||
        var songData = playlistResult.Songs[0];
 | 
			
		||||
 | 
			
		||||
        var title = songData.Name;
 | 
			
		||||
        var artist = songData.Artists[0].Name;
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS8604 // Possible null reference argument.
 | 
			
		||||
#pragma warning disable CS8604 // Possible null reference argument.
 | 
			
		||||
        var song = new Song
 | 
			
		||||
        {
 | 
			
		||||
            Name = title,
 | 
			
		||||
            Artist = artist,
 | 
			
		||||
            Url = songUri.ToString(),
 | 
			
		||||
            Provider = SongProvider.YouTube,
 | 
			
		||||
            SpotifyId = this.LookupSpotifyId(title, artist)
 | 
			
		||||
            Provider = SongProvider.YoutubeMusic,
 | 
			
		||||
            SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
 | 
			
		||||
        };
 | 
			
		||||
#pragma warning restore CS8604 // Possible null reference argument.
 | 
			
		||||
#pragma warning restore CS8604 // Possible null reference argument.
 | 
			
		||||
 | 
			
		||||
        return song;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,60 +1,47 @@
 | 
			
		||||
using AngleSharp;
 | 
			
		||||
using AngleSharp.Dom;
 | 
			
		||||
using AngleSharp.Html.Dom;
 | 
			
		||||
using YouTubeMusicAPI.Client;
 | 
			
		||||
 | 
			
		||||
public class YoutubeValidator : UriBasedSongValidatorBase
 | 
			
		||||
{
 | 
			
		||||
    private YouTubeMusicClient youtubeClient;
 | 
			
		||||
    
 | 
			
		||||
    public YoutubeValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
 | 
			
		||||
    {
 | 
			
		||||
        youtubeClient = new("AT");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string UriValidatorRegex => @"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
 | 
			
		||||
 | 
			
		||||
    public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        using (HttpClient httpClient = new HttpClient())
 | 
			
		||||
        {
 | 
			
		||||
            var response = await httpClient.GetAsync(songUri);
 | 
			
		||||
            var config = Configuration.Default.WithDefaultLoader();
 | 
			
		||||
            var context = BrowsingContext.New(config);
 | 
			
		||||
            using (var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
 | 
			
		||||
            {
 | 
			
		||||
#pragma warning disable CS8602 // Dereference of a possibly null reference.
 | 
			
		||||
                var documentContents = (document.ChildNodes[1] as HtmlElement).InnerHtml;
 | 
			
		||||
#pragma warning restore CS8602 // Dereference of a possibly null reference.
 | 
			
		||||
                var titleElement = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0];
 | 
			
		||||
                var artistParentElement = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0];
 | 
			
		||||
        return this.CanValidateUri(songUri);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
                return titleElement != null && artistParentElement != null && artistParentElement.Children.Length > 0;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    public override SongProvider GetSongProvider()
 | 
			
		||||
    {
 | 
			
		||||
        return SongProvider.YouTube;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<Song> ValidateAsync(Uri songUri)
 | 
			
		||||
    {
 | 
			
		||||
        var title = string.Empty;
 | 
			
		||||
        var artist = string.Empty;
 | 
			
		||||
        var match = this.GetUriMatch(songUri);
 | 
			
		||||
        var songId = match.Groups[4].Value;
 | 
			
		||||
 | 
			
		||||
        using (HttpClient httpClient = new HttpClient())
 | 
			
		||||
        {
 | 
			
		||||
            var response = await httpClient.GetAsync(songUri);
 | 
			
		||||
            var config = Configuration.Default.WithDefaultLoader();
 | 
			
		||||
            var context = BrowsingContext.New(config);
 | 
			
		||||
            using (var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
 | 
			
		||||
            {
 | 
			
		||||
                title = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0]?.InnerHtml;
 | 
			
		||||
                artist = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0]?.Children[0]?.InnerHtml;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var songData = await youtubeClient.GetSongVideoInfoAsync(songId);
 | 
			
		||||
 | 
			
		||||
        var title = songData.Name;
 | 
			
		||||
        var artist = songData.Artists[0].Name;
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS8604 // Possible null reference argument.
 | 
			
		||||
#pragma warning disable CS8604 // Possible null reference argument.
 | 
			
		||||
        var song = new Song
 | 
			
		||||
        {
 | 
			
		||||
            Name = title,
 | 
			
		||||
            Artist = artist,
 | 
			
		||||
            Url = songUri.ToString(),
 | 
			
		||||
            Provider = SongProvider.YouTube,
 | 
			
		||||
            SpotifyId = this.LookupSpotifyId(title, artist)
 | 
			
		||||
            SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
 | 
			
		||||
        };
 | 
			
		||||
#pragma warning restore CS8604 // Possible null reference argument.
 | 
			
		||||
#pragma warning restore CS8604 // Possible null reference argument.
 | 
			
		||||
 | 
			
		||||
        return song;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
    <PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5" />
 | 
			
		||||
    <PackageReference Include="Scalar.AspNetCore" Version="2.1.*" />
 | 
			
		||||
    <PackageReference Include="SpotifyAPI.Web" Version="7.2.1" />
 | 
			
		||||
    <PackageReference Include="YouTubeMusicAPI" Version="2.2.8" />
 | 
			
		||||
    <PackageReference Include="System.DirectoryServices.Protocols" Version="*" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -2423,6 +2423,9 @@
 | 
			
		||||
                "edit_timestamp": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                },
 | 
			
		||||
                "link_preview": {
 | 
			
		||||
                    "$ref": "#/definitions/data.LinkPreviewType"
 | 
			
		||||
                },
 | 
			
		||||
                "mentions": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "items": {
 | 
			
		||||
@@ -2817,6 +2820,23 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "data.LinkPreviewType": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "base64_thumbnail": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "description": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "title": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "url": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "data.MessageMention": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user