fix: some cleanup and fixing runtime bugs, refs NOISSUE

This commit is contained in:
simon 2025-05-31 13:41:03 +02:00
parent dbd83ebb6a
commit 0d2ec3712e
23 changed files with 269 additions and 128 deletions

View File

@ -9,7 +9,7 @@ RUN apt update && apt install libldap-2.5-0 -y
# Restore as distinct layers
RUN dotnet restore ./song_of_the_day/song_of_the_day.csproj
# Build and publish a release
RUN dotnet publish ./song_of_the_day/song_of_the_day.csproj -o out
RUN dotnet publish ./song_of_the_day/song_of_the_day.csproj -o out /p:EnvironmentName=Production
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0

View File

@ -2,7 +2,7 @@
.PHONY: issetup
issetup:
@[ -f .git/hooks/commit-msg ] || [ $SKIP_MAKE_SETUP_CHECK = "true" ] || (echo "You must run 'make setup' first to initialize the repo!" && exit 1)
@[ -f .git/hooks/commit-msg ] || [ ${SKIP_MAKE_SETUP_CHECK} = "true" ] || (echo "You must run 'make setup' first to initialize the repo!" && exit 1)
.PHONY: setup
setup:

View File

@ -2,3 +2,5 @@
# CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
dotnet_diagnostic.CS8981.severity = none
dotnet_diagnostic.CS8602.severity = none
dotnet_diagnostic.CS8604.severity = none

46
song_of_the_day/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,46 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: SongOfTheDay Debug",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "publish Debug",
"program": "${workspaceFolder}/bin/Debug/net9.0/publish/song_of_the_day.dll",
"cwd": "${workspaceFolder}/bin/Debug/net9.0/publish/",
"args": [
"/p:EnvironmentName=Development"
],
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"Logging__LogLevel__Microsoft": "Information"
}
},
{
"name": "C#: SongOfTheDay Production",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "publish Release",
"program": "${workspaceFolder}/bin/Release/net9.0/publish/song_of_the_day.dll",
"cwd": "${workspaceFolder}/bin/Release/net9.0/publish/",
"args": [
"/p:EnvironmentName=Production"
],
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Production",
"Logging__LogLevel__Microsoft": "Information"
}
}
]
}

47
song_of_the_day/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,47 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "publish Debug",
"command": "dotnet",
"type": "shell",
"args": [
"publish",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary",
"/p:EnvironmentName=Development",
"-c",
"Debug"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "publish Release",
"command": "dotnet",
"type": "shell",
"args": [
"publish",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary",
"/p:EnvironmentName=Production",
"-c",
"Release"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}

View File

@ -1,15 +1,16 @@
public class LdapAuthenticationService : IAuthenticationService
{
private readonly IConfiguration _configuration;
private LdapIntegration _ldapIntegration;
public LdapAuthenticationService(IConfiguration configuration)
public LdapAuthenticationService(IConfiguration configuration, LdapIntegration ldapIntegration)
{
_configuration = configuration;
_ldapIntegration = ldapIntegration;
}
public bool Authenticate(string username, string password)
{
var ldapInstance = LdapIntegration.Instance;
return ldapInstance == null ? false : ldapInstance.TestLogin(username, password);
return _ldapIntegration == null ? false : _ldapIntegration.TestLogin(username, password);
}
}

View File

@ -2,11 +2,13 @@ public class PhoneClaimCodeProviderService
{
private Dictionary<string, string> _phoneClaimCodes;
private Dictionary<string, string> _phoneClaimNumbers;
private SignalIntegration _signalIntegration;
public PhoneClaimCodeProviderService()
public PhoneClaimCodeProviderService(SignalIntegration signalIntegration)
{
_phoneClaimCodes = new Dictionary<string, string>();
_phoneClaimNumbers = new Dictionary<string, string>();
_signalIntegration = signalIntegration;
}
private static Random random = new Random();
@ -32,7 +34,10 @@ public class PhoneClaimCodeProviderService
_phoneClaimNumbers[username] = phoneNumber;
}
await SignalIntegration.Instance.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
if (_signalIntegration != null)
{
await _signalIntegration.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
}
}
public string ValidateClaimCodeForUser(string code, string username)

View File

@ -6,8 +6,6 @@ using System.Linq;
public class LdapIntegration
{
public static LdapIntegration? Instance;
private readonly string[] attributesToQuery = new string[]
{
"uid",
@ -16,12 +14,13 @@ public class LdapIntegration
"mail"
};
public LdapIntegration(string uri, int port, string adminBind, string adminPass)
public LdapIntegration(ILogger<LdapIntegration> logger)
{
this.Uri = uri;
this.Port = port;
this.AdminBind = adminBind;
this.AdminPass = adminPass;
this.Uri = AppConfiguration.Instance.LDAPConfig.LDAPserver;
this.Port = AppConfiguration.Instance.LDAPConfig.Port;
this.AdminBind = AppConfiguration.Instance.LDAPConfig.Username;
this.AdminPass = AppConfiguration.Instance.LDAPConfig.Password;
this.logger = logger;
}
private string Uri { get; set; }
@ -32,6 +31,8 @@ public class LdapIntegration
private string AdminPass { get; set; }
private ILogger<LdapIntegration> logger { get; set; }
public bool TestLogin(string username, string password)
{
try

View File

@ -1,29 +1,28 @@
using System.Collections;
using System.ComponentModel;
using song_of_the_day;
public class SignalIntegration
{
public static SignalIntegration? Instance;
private readonly ILogger<SignalIntegration> logger;
private readonly ILogger logger;
public SignalIntegration(string uri, int port, string phoneNumber)
public SignalIntegration(ILogger<SignalIntegration> logger)
{
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
this.logger = factory.CreateLogger("SignalIntegration");
var uri = AppConfiguration.Instance.SignalAPIEndpointUri;
var port = int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort);
var phoneNumber = AppConfiguration.Instance.HostPhoneNumber;
this.logger = logger;
var http = new HttpClient()
{
BaseAddress = new Uri(uri + ":" + port)
};
apiClient = new song_of_the_day.swaggerClient(http);
apiClient = new swaggerClient(http);
apiClient.BaseUrl = "";
this.phoneNumber = phoneNumber;
}
private song_of_the_day.swaggerClient apiClient;
private swaggerClient apiClient;
private string phoneNumber;
@ -83,6 +82,11 @@ public class SignalIntegration
public async Task IntroduceUserAsync(User user)
{
if (user == null || user.SignalMemberId == null)
{
logger.LogWarning("Attempt to introduce unknown user was aborted.");
return;
}
await this.SendMessageToUserAsync("Hi, my name is Proggy and I am your friendly neighborhood *Song of the Day* bot!", user.SignalMemberId);
await this.SendMessageToUserAsync("You are receiving this message because you have been invited to a *Song of the Day* community group.", user.SignalMemberId);
await this.SendMessageToUserAsync("In that community group I will pick a person at random each day at 8 AM and encourage them to share a song with the rest of the community.", user.SignalMemberId);

View File

@ -3,8 +3,8 @@
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Where(u => u.LdapUserName == User.Identity.Name);
return user.Any();
var user = dci.Users?.Where(u => u.LdapUserName == User.Identity.Name);
return user == null ? false : user.Any();
}
}
}
@ -18,7 +18,7 @@
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/sotd.styles.css" asp-append-version="true" />
<link rel="stylesheet" href="~/song_of_the_day.styles.css" asp-append-version="true" />
</head>
<body>
<header>
@ -37,13 +37,15 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a>
</li>
@if (this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber())
@if (this.User != null && this.User.Identity != null &&
this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber())
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
</li>
}
@if (this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
@if (this.User != null && this.User.Identity != null &&
this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/SubmitSongs">Submit Songs</a>

View File

@ -1,7 +1,7 @@
@using Microsoft.AspNetCore.Authentication
<div class="loginform">
@if (!this.User.Identity.IsAuthenticated)
@if (this.User == null || this.User.Identity == null || !this.User.Identity.IsAuthenticated)
{
<form method="post" action="Auth/Login">
<div>

View File

@ -17,20 +17,26 @@ public class SubmitSongsModel : PageModel
{
_logger = logger;
this.songResolver = songResolver;
_submitUrl = string.Empty;
SongData = new Song();
}
[BindProperty]
public bool IsValidUrl { get; set; } = true;
[BindProperty]
public string SubmitUrl {
get {
public string SubmitUrl
{
get
{
return _submitUrl;
}
set {
set
{
_submitUrl = value.ToString();
Uri? newValue = default;
try {
try
{
newValue = new Uri(_submitUrl);
}
catch (UriFormatException)
@ -41,7 +47,7 @@ public class SubmitSongsModel : PageModel
IsValidUrl = true;
if(this.songResolver.CanValidate(newValue))
if (this.songResolver.CanValidate(newValue))
{
this.SongData = this.songResolver.ResolveSongAsync(newValue).Result;
}
@ -49,9 +55,13 @@ public class SubmitSongsModel : PageModel
}
[BindProperty]
public bool CanSubmit { get {
return !string.IsNullOrEmpty(SongData?.Artist) && ! string.IsNullOrEmpty(SongData?.Name);
} }
public bool CanSubmit
{
get
{
return !string.IsNullOrEmpty(SongData?.Artist) && !string.IsNullOrEmpty(SongData?.Name);
}
}
[BindProperty]
public Song SongData { get; set; }
@ -66,6 +76,6 @@ public class SubmitSongsModel : PageModel
{
var songUrl = Request.Query["SubmitUrl"];
this.SubmitUrl = songUrl.ToString();
return Partial("_SongPartial", SongData);;
return Partial("_SongPartial", SongData); ;
}
}

View File

@ -29,7 +29,7 @@ public class SuggestionHelpersModel : PageModel
{
using (var dci = DataContext.Instance)
{
this.SuggestionHelpers = dci.SuggestionHelpers.ToList();
this.SuggestionHelpers = dci.SuggestionHelpers == null ? new List<SuggestionHelper>() : dci.SuggestionHelpers.ToList();
}
}
@ -42,9 +42,9 @@ public class SuggestionHelpersModel : PageModel
Title = this.NewSuggestionTitle,
Description = this.NewSuggestionDescription
};
dci.SuggestionHelpers.Add(newHelper);
dci.SuggestionHelpers?.Add(newHelper);
dci.SaveChanges();
this.SuggestionHelpers = dci.SuggestionHelpers.ToList();
this.SuggestionHelpers = dci.SuggestionHelpers == null ? new List<SuggestionHelper>() : dci.SuggestionHelpers.ToList();
}
this.NewSuggestionDescription = "";
this.NewSuggestionTitle = "";

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.VisualBasic;
using SpotifyAPI.Web;
namespace sotd.Pages;
@ -11,6 +12,7 @@ public class UnclaimedPhoneNumbersModel : PageModel
public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger)
{
_logger = logger;
UnclaimedUsers = new List<User>();
}
public int userId { get; set; }
@ -22,7 +24,7 @@ public class UnclaimedPhoneNumbersModel : PageModel
{
using (var dci = DataContext.Instance)
{
this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
this.UnclaimedUsers = dci.Users == null ? new List<User>(): dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
}
}

View File

@ -25,9 +25,9 @@ public class UserModel : PageModel
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Find(userIndex);
this.UserName = user.Name;
this.UserNickName = user.NickName;
var user = dci.Users?.Find(userIndex);
this.UserName = user == null ? string.Empty : (user.Name ?? string.Empty);
this.UserNickName = user == null ? string.Empty : (user.NickName ?? string.Empty);
this.userId = userIndex;
}
}
@ -36,10 +36,13 @@ public class UserModel : PageModel
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Find(userIndex);
user.NickName = this.UserNickName;
dci.SaveChanges();
this.UserName = user.Name;
var user = dci.Users?.Find(userIndex);
if (user != null)
{
user.NickName = this.UserNickName;
dci.SaveChanges();
this.UserName = user.Name ?? string.Empty;
}
}
}
}

View File

@ -1,31 +1,37 @@

using Scalar.AspNetCore;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.DirectoryServices.Protocols;
using System.Runtime.CompilerServices;
SignalIntegration.Instance = new SignalIntegration(AppConfiguration.Instance.SignalAPIEndpointUri,
int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort),
AppConfiguration.Instance.HostPhoneNumber);
LdapIntegration.Instance = new LdapIntegration(AppConfiguration.Instance.LDAPConfig.LDAPserver,
AppConfiguration.Instance.LDAPConfig.Port,
AppConfiguration.Instance.LDAPConfig.Username,
AppConfiguration.Instance.LDAPConfig.Password);
var builder = WebApplication.CreateBuilder(args);
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
var logger = factory.CreateLogger("SongResolver");
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddLogging((ILoggingBuilder b) => b.ClearProviders().AddConsole());
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
});
builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
builder.Services.AddSingleton<SignalIntegration>();
builder.Services.AddSingleton<LdapIntegration>();
builder.Services.AddSingleton<SongResolver>();
var app = builder.Build();
var logger = app.Logger;
logger.LogTrace("Setting up user check timer");
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
userCheckTimer.OnOccurence += async (s, ea) =>
{
var memberList = await SignalIntegration.Instance.GetMemberListAsync();
var signalIntegration = app.Services.GetService<SignalIntegration>();
var memberList = await signalIntegration.GetMemberListAsync();
var dci = DataContext.Instance;
var needsSaving = false;
foreach (var memberId in memberList)
@ -33,7 +39,7 @@ userCheckTimer.OnOccurence += async (s, ea) =>
var foundUser = dci.Users?.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
if (foundUser == null)
{
var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId);
var newUserContact = await signalIntegration.GetContactAsync(memberId);
logger.LogDebug("New user:");
logger.LogDebug($" Name: {newUserContact.Name}");
logger.LogDebug($" MemberId: {memberId}");
@ -74,7 +80,8 @@ userIntroTimer.OnOccurence += async (s, ea) =>
bool needsSaving = false;
foreach (var user in introUsers)
{
await SignalIntegration.Instance.IntroduceUserAsync(user);
var signalIntegration = app.Services.GetService<SignalIntegration>();
await signalIntegration.IntroduceUserAsync(user);
user.IsIntroduced = true;
needsSaving = true;
}
@ -137,12 +144,14 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
if (luckyUser.SignalMemberId is string signalId)
{
await dci.SongSuggestions.AddAsync(newSongSuggestion);
luckyUser.WasChosenForSuggestionThisRound = true;
await dci.SaveChangesAsync();
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*");
await SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId);
await SignalIntegration.Instance.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);
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 dci.DisposeAsync();
};
@ -151,10 +160,11 @@ var startUserAssociationProcess = async (User userToAssociate) =>
{
if (userToAssociate.SignalMemberId is string signalId)
{
await SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId);
var signalIntegration = app.Services.GetService<SignalIntegration>();
await signalIntegration.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId);
await signalIntegration.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId);
await signalIntegration.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId);
await signalIntegration.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId);
}
};
@ -187,30 +197,20 @@ ldapAssociationTimer.OnOccurence += async (s, ea) =>
await dci.DisposeAsync();
};
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
});
builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
builder.Services.AddSingleton<SongResolver>();
var app = builder.Build();
// only start interaction timers in production builds
// for local/development testing we want those disabled
if (!app.Environment.IsDevelopment())
{
logger.LogTrace("Starting timer for scheduled processes.");
userCheckTimer.Start();
userIntroTimer.Start();
pickOfTheDayTimer.Start();
ldapAssociationTimer.Start();
}
else
{
logger.LogTrace("This is a debug build - scheduled processes are disabled.");
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())

View File

@ -4,17 +4,17 @@ public class SongResolver
{
private readonly IEnumerable<ISongValidator> _songValidators;
private readonly ILogger logger;
private readonly ILogger<SongResolver> logger;
public SongResolver()
public SongResolver(ILogger<SongResolver> logger)
{
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
this.logger = factory.CreateLogger("SongResolver");
this.logger = logger;
this._songValidators = new List<ISongValidator>();
foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
.Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base")))) {
.Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base"))))
{
if (Activator.CreateInstance(mytype) is ISongValidator validator)
{
logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name);
@ -32,23 +32,25 @@ public class SongResolver
if (!await validator.CanExtractSongMetadataAsync(songUri))
{
this.logger.LogWarning("Cannot extract metadata for song URI: {SongUri}", songUri);
return new Song {
Artist = "Unknown Artist",
Name = "Unknown Title",
Url = songUri.ToString(),
Provider = SongProvider.PlainHttp,
};
}
return await validator.ValidateAsync(songUri);
}
}
return new Song {
return new Song
{
Artist = "Unknown Artist",
Name = "Unknown Title",
Url = songUri.ToString(),
Provider = SongProvider.PlainHttp,
};
}
return await validator.ValidateAsync(songUri);
}
}
return new Song
{
Artist = "Unknown Artist",
Name = "Unknown Title",
Url = songUri.ToString(),
Provider = SongProvider.PlainHttp,
};
}
public bool CanValidate(Uri songUri)

View File

@ -14,9 +14,9 @@ public class SpotifyValidator : UriBasedSongValidatorBase
spotifyApiClient = new SpotifyApiClient();
}
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
return this.CanValidateUri(songUri);
return Task.FromResult(this.CanValidateUri(songUri));
}
public override async Task<Song> ValidateAsync(Uri songUri)
@ -25,7 +25,7 @@ public class SpotifyValidator : UriBasedSongValidatorBase
var trackIdMatch = regexp.Match(songUri.ToString()).Groups[2].Value;
var track = await spotifyApiClient.GetTrackByIdAsync(trackIdMatch);
var song = new Song
{
Name = track.Name,

View File

@ -5,9 +5,9 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
{
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
return this.CanValidateUri(songUri);
return Task.FromResult(this.CanValidateUri(songUri));
}
public override async Task<Song> ValidateAsync(Uri songUri)
@ -15,12 +15,12 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
var title = string.Empty;
var artist = string.Empty;
using(HttpClient httpClient = new HttpClient())
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())))
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;
@ -28,7 +28,9 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
artist = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".byline")?.InnerHtml;
}
}
#pragma warning disable CS8604 // Possible null reference argument.
#pragma warning disable CS8604 // Possible null reference argument.
var song = new Song
{
Name = title,
@ -37,6 +39,8 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
Provider = SongProvider.YouTube,
SpotifyId = this.LookupSpotifyId(title, artist)
};
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
return song;
}

View File

@ -8,14 +8,16 @@ public class YoutubeValidator : UriBasedSongValidatorBase
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
using(HttpClient httpClient = new HttpClient())
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())))
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];
@ -29,18 +31,20 @@ public class YoutubeValidator : UriBasedSongValidatorBase
var title = string.Empty;
var artist = string.Empty;
using(HttpClient httpClient = new HttpClient())
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())))
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;
}
}
#pragma warning disable CS8604 // Possible null reference argument.
#pragma warning disable CS8604 // Possible null reference argument.
var song = new Song
{
Name = title,
@ -49,6 +53,8 @@ public class YoutubeValidator : UriBasedSongValidatorBase
Provider = SongProvider.YouTube,
SpotifyId = this.LookupSpotifyId(title, artist)
};
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
return song;
}

View File

@ -8,7 +8,7 @@ public class SpotifyApiClient
{
var config = SpotifyClientConfig.CreateDefault()
.WithAuthenticator(new ClientCredentialsAuthenticator(
AppConfiguration.Instance.SpotifyClientId,
AppConfiguration.Instance.SpotifyClientId,
AppConfiguration.Instance.SpotifyClientSecret));
_spotifyClient = new SpotifyClient(config);

View File

@ -2,8 +2,11 @@
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": "Trace",
"Microsoft.AspNetCore": "Warning",
"SongResolver": "Trace",
"SignalIntegration": "Trace",
"SongOfTheDay": "Trace"
}
}
}

View File

@ -1,8 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": "Trace",
"Microsoft.AspNetCore": "Warning",
"SongResolver": "Information",
"SignalIntegration": "Information",
"SongOfTheDay": "Trace"
}
},
"AllowedHosts": "*"