feat: implement song submission support, refs #5

This commit is contained in:
2025-06-05 00:14:53 +02:00
parent 0d2ec3712e
commit 220f4d7ffd
27 changed files with 943 additions and 191 deletions

View File

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

View File

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

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

View File

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

View File

@@ -45,4 +45,4 @@ button.accept-policy {
width: 100%;
white-space: nowrap;
line-height: 60px;
}
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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