feat: implement song submission support, refs #5
This commit is contained in:
parent
0d2ec3712e
commit
220f4d7ffd
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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user