Compare commits

32 Commits
0.3.1 ... 0.6.7

Author SHA1 Message Date
e0b0d6b98c release: version 0.6.7 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 20s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m23s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m29s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m53s
2025-07-20 17:24:16 +02:00
74a8c7dbe8 fix: attempted bugfix for crashing process on invalid spotify access token, refs NOISSUE 2025-07-20 17:24:14 +02:00
da2a32ecfc release: version 0.6.6 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 16s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / deploy (push) Successful in 1m33s
2025-07-20 17:14:53 +02:00
ef8c8fb867 fix: add additional logging, refs NOISSUE 2025-07-20 17:14:49 +02:00
567f192c46 release: version 0.6.5 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 15s
Build Docker image / deploy (push) Successful in 1m29s
2025-07-20 17:05:12 +02:00
ff5c4588c9 fix: configurable Cron schedules, refs NOISSUE 2025-07-20 17:05:09 +02:00
979b7e9fed release: version 0.6.4 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 19s
Build Docker image / deploy (push) Successful in 1m29s
2025-07-20 16:46:44 +02:00
7457e77867 fix: better data model for liked songs, refs NOISSUE 2025-07-20 16:46:34 +02:00
1f4b5ad34e release: version 0.6.3 🚀
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 42s
Build Docker image / deploy (push) Successful in 1m50s
2025-07-20 03:34:18 +02:00
10cfec30f9 fix: issues with index page for unauthenticated users, refs NOISSUE 2025-07-20 03:34:09 +02:00
d41c4d2b2d release: version 0.6.2 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 1m1s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m28s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 1m46s
SonarQube Scan / SonarQube Trigger (push) Failing after 5m37s
2025-07-20 03:26:47 +02:00
bc7b16ecb4 fix: build issues, refs NOISSUE 2025-07-20 03:26:17 +02:00
081523b510 release: version 0.6.1 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 1m5s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m29s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2025-07-20 03:24:40 +02:00
856e30aacc fix: remove local debugging configs, refs NOISSUE 2025-07-20 03:24:35 +02:00
82946ac812 release: version 0.6.0 🚀
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m27s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / Create Release (push) Successful in 8s
SonarQube Scan / SonarQube Trigger (push) Failing after 5m21s
Build Docker image / deploy (push) Failing after 4m7s
2025-07-20 03:19:21 +02:00
5b72e25636 fix: formatting, refs NOISSUE 2025-07-20 03:19:16 +02:00
2e876ad628 feat: song likes and initial implementation of Spotify playlist support, refs #9 2025-07-20 03:18:22 +02:00
8b91a13095 release: version 0.5.0 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 19s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m44s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m52s
Build Docker image / deploy (push) Successful in 4m32s
2025-07-06 17:01:23 +02:00
27a5ca6b74 feat: add Navidrome song validator, refs #5 2025-07-06 17:01:07 +02:00
Simon Diesenreiter
8a36606dee release: version 0.4.2 🚀
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m6s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / Create Release (push) Successful in 9s
Build Docker image / deploy (push) Successful in 1m17s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m48s
2025-06-25 10:02:16 +02:00
Simon Diesenreiter
dfc02f6907 fix: broken build, refs NOISSUE 2025-06-25 10:02:09 +02:00
Simon Diesenreiter
887a8a7b6a release: version 0.4.1 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 20s
CI / linter (9.0.X, ubuntu-latest) (push) Successful in 1m14s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Failing after 1m30s
SonarQube Scan / SonarQube Trigger (push) Failing after 5m8s
Build Docker image / deploy (push) Failing after 4m56s
2025-06-25 09:48:32 +02:00
Simon Diesenreiter
4b18003aa8 fix: fix formatting, refs NOISSUE 2025-06-25 09:48:27 +02:00
c0bee8fd3c fix: URL type, refs NOISSUE
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m20s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m48s
2025-06-25 00:42:34 -07:00
23afbc1009 release: version 0.4.0 🚀
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 2m16s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / Create Release (push) Successful in 10s
Build Docker image / deploy (push) Successful in 2m10s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m51s
2025-06-05 00:15:00 +02:00
220f4d7ffd feat: implement song submission support, refs #5 2025-06-05 00:14:53 +02:00
0d2ec3712e fix: some cleanup and fixing runtime bugs, refs NOISSUE 2025-05-31 13:41:03 +02:00
Simon Diesenreiter
dbd83ebb6a feat: basic initial implementation of spotify client link validator and song submission form refs: NOISSUE
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m3s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m47s
2025-05-30 22:51:44 +02:00
Simon Diesenreiter
010316aa70 release: version 0.3.3 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 18s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m6s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m5s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m45s
2025-05-26 09:23:29 +02:00
Simon Diesenreiter
d416242712 fix: save DateTime as UTC, refs NOISSUE 2025-05-26 09:23:25 +02:00
e9a824c6ef release: version 0.3.2 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 30s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m28s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m48s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m55s
2025-05-25 21:36:50 +02:00
e0f206bc0c fix: exception thrown on LastOrDefault(), refs NOISSUE 2025-05-25 21:36:34 +02:00
66 changed files with 4740 additions and 134 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

@@ -5,10 +5,168 @@ Changelog
(unreleased)
------------
Fix
~~~
- Attempted bugfix for crashing process on invalid spotify access token,
refs NOISSUE. [Simon Diesenreiter]
0.6.6 (2025-07-20)
------------------
Fix
~~~
- Add additional logging, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.6.5 (2025-07-20)
------------------
Fix
~~~
- Configurable Cron schedules, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.6.4 (2025-07-20)
------------------
Fix
~~~
- Better data model for liked songs, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.6.3 (2025-07-20)
------------------
Fix
~~~
- Issues with index page for unauthenticated users, refs NOISSUE. [Simon
Diesenreiter]
Other
~~~~~
0.6.2 (2025-07-20)
------------------
Fix
~~~
- Build issues, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.6.1 (2025-07-20)
------------------
Fix
~~~
- Remove local debugging configs, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.6.0 (2025-07-20)
------------------
Fix
~~~
- Formatting, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
- Feat: song likes and initial implementation of Spotify playlist
support, refs #9. [Simon Diesenreiter]
0.5.0 (2025-07-06)
------------------
- Feat: add Navidrome song validator, refs #5. [Simon Diesenreiter]
0.4.2 (2025-06-25)
------------------
Fix
~~~
- Broken build, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.4.1 (2025-06-25)
------------------
Fix
~~~
- Fix formatting, refs NOISSUE. [Simon Diesenreiter]
- URL type, refs NOISSUE. [simon]
Other
~~~~~
0.4.0 (2025-06-04)
------------------
Fix
~~~
- Some cleanup and fixing runtime bugs, refs NOISSUE. [Simon
Diesenreiter]
Other
~~~~~
- Feat: implement song submission support, refs #5. [Simon Diesenreiter]
- Feat: basic initial implementation of spotify client link validator
and song submission form refs: NOISSUE. [Simon Diesenreiter]
0.3.3 (2025-05-26)
------------------
Fix
~~~
- Save DateTime as UTC, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.3.2 (2025-05-25)
------------------
Fix
~~~
- Exception thrown on LastOrDefault(), refs NOISSUE. [Simon
Diesenreiter]
Other
~~~~~
0.3.1 (2025-05-24)
------------------
Fix
~~~
- Fix build errors, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.3.0 (2025-05-24)
------------------

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

@@ -0,0 +1,6 @@
[*.cs]
# 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.TestLogin(username, password);
return _ldapIntegration == null ? false : _ldapIntegration.TestLogin(username, password);
}
}

View File

@@ -2,23 +2,24 @@ 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();
private static string RandomString(int length)
{
Random random = new Random();
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
public void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
public async void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
{
var generatedCode = string.Empty;
if (IsCodeGeneratedForUser(username))
@@ -32,7 +33,10 @@ public class PhoneClaimCodeProviderService
_phoneClaimNumbers[username] = phoneNumber;
}
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

@@ -20,6 +20,8 @@ public class AppConfiguration
var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND");
this.SpotifyClientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") ?? "0c59b625470b4ad1b70743e0254d17fd";
this.SpotifyClientSecret = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET") ?? "04daaebd42fc47909c5cbd1f5cf23555";
this.LDAPConfig = new ConfigurationAD()
{
Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
@@ -31,6 +33,13 @@ public class AppConfiguration
CrewGroup = $"cn={userGroupName},ou=groups,dc=disi,dc=dev",
ManagerGroup = $"cn={managersGroupName},ou=groups,dc=disi,dc=dev"
};
this.UserCheckTimerSchedule = Environment.GetEnvironmentVariable("USER_CHECK_TIMER_SCHEDULE") ?? "*/1 * * * *";
this.LikePlaylistCheckTimerSchedule = Environment.GetEnvironmentVariable("LIKE_PLAYLIST_CHECK_TIMER_SCHEDULE") ?? "*/10 * * * *";
this.UserIntroCheckTimerSchedule = Environment.GetEnvironmentVariable("USER_INTRO_TIMER_SCHEDULE") ?? "*/1 * * * *";
this.PickOfTheDayCheckTimerSchedule = Environment.GetEnvironmentVariable("PICK_OF_THE_DAY_TIMER_SCHEDULE") ?? "0 8 * * *";
this.LdapAssociationTimerSchedule = Environment.GetEnvironmentVariable("LDAP_ASSOCIATION_TIMER_SCHEDULE") ?? "*/10 * * * *";
this.MessageSyncTimerSchedule = Environment.GetEnvironmentVariable("MESSAGE_SYNC_TIMER_SCHEDULE") ?? "*/10 * * * *";
}
public string SignalAPIEndpointUri
@@ -83,6 +92,16 @@ public class AppConfiguration
get; private set;
}
public string SpotifyClientId
{
get; private set;
}
public string SpotifyClientSecret
{
get; private set;
}
public bool UseBotTag
{
get; private set;
@@ -97,4 +116,34 @@ public class AppConfiguration
{
get; private set;
}
public string UserCheckTimerSchedule
{
get; private set;
}
public string LikePlaylistCheckTimerSchedule
{
get; private set;
}
public string UserIntroCheckTimerSchedule
{
get; private set;
}
public string PickOfTheDayCheckTimerSchedule
{
get; private set;
}
public string LdapAssociationTimerSchedule
{
get; private set;
}
public string MessageSyncTimerSchedule
{
get; private set;
}
}

View File

@@ -10,7 +10,7 @@ public class AuthController : Controller
public async Task<IActionResult> Login(string username, string password)
{
var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
if (ldapService.Authenticate(username, password))
if (ldapService != null && ldapService.Authenticate(username, password))
{
var claims = new[] { new Claim(ClaimTypes.Name, username) };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

View File

@@ -12,9 +12,23 @@ public class DataContext : DbContext
public DbSet<Song>? Songs { get; set; }
public DbSet<SongSuggestion>? SongSuggestions { get; set; }
public DbSet<SuggestionHelper>? SuggestionHelpers { get; set; }
public DbSet<SmartPlaylistDefinition>? SmartPlaylistDefinitions { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseNpgsql($"Host={AppConfiguration.Instance.DatabaseUri}:{AppConfiguration.Instance.DatabasePort};"
+ $"Username={AppConfiguration.Instance.DatabaseUser};Password={AppConfiguration.Instance.DatabasePW};"
+ $"Database={AppConfiguration.Instance.DatabaseName}");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// configures one-to-many relationship
modelBuilder.Entity<User>()
.HasMany(u => u.LikedSongs)
.WithMany(s => s.LikedBy)
.UsingEntity(
"LikedSongs",
r => r.HasOne(typeof(Song)).WithMany().HasForeignKey("SongId").HasPrincipalKey(nameof(Song.SongId)),
l => l.HasOne(typeof(User)).WithMany().HasForeignKey("UserId").HasPrincipalKey(nameof(User.UserId)),
j => j.HasKey("SongId", "UserId"));
}
}

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

View File

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

View File

@@ -0,0 +1,268 @@
// <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("20250719185147_adding song likes and playlists")]
partial class addingsonglikesandplaylists
{
/// <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("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
b.HasIndex("UserId");
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
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("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
b.HasOne("User", null)
.WithMany("LikedSongs")
.HasForeignKey("UserId");
});
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");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
modelBuilder.Entity("User", b =>
{
b.Navigation("LikedSongs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,170 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class addingsonglikesandplaylists : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SmartPlaylistDefinitionId",
table: "SuggestionHelpers",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SmartPlaylistDefinitionId",
table: "Songs",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SmartPlaylistDefinitionId1",
table: "Songs",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "Songs",
type: "integer",
nullable: true);
migrationBuilder.CreateTable(
name: "SmartPlaylistDefinitions",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Title = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
CreatedByUserId = table.Column<int>(type: "integer", nullable: true),
IncludesUnCategorizedSongs = table.Column<bool>(type: "boolean", nullable: false),
IncludesLikedSongs = table.Column<bool>(type: "boolean", nullable: false),
SpotifyPlaylistId = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SmartPlaylistDefinitions", x => x.Id);
table.ForeignKey(
name: "FK_SmartPlaylistDefinitions_Users_CreatedByUserId",
column: x => x.CreatedByUserId,
principalTable: "Users",
principalColumn: "UserId");
});
migrationBuilder.CreateIndex(
name: "IX_SuggestionHelpers_SmartPlaylistDefinitionId",
table: "SuggestionHelpers",
column: "SmartPlaylistDefinitionId");
migrationBuilder.CreateIndex(
name: "IX_Songs_SmartPlaylistDefinitionId",
table: "Songs",
column: "SmartPlaylistDefinitionId");
migrationBuilder.CreateIndex(
name: "IX_Songs_SmartPlaylistDefinitionId1",
table: "Songs",
column: "SmartPlaylistDefinitionId1");
migrationBuilder.CreateIndex(
name: "IX_Songs_UserId",
table: "Songs",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_SmartPlaylistDefinitions_CreatedByUserId",
table: "SmartPlaylistDefinitions",
column: "CreatedByUserId");
migrationBuilder.AddForeignKey(
name: "FK_Songs_SmartPlaylistDefinitions_SmartPlaylistDefinitionId",
table: "Songs",
column: "SmartPlaylistDefinitionId",
principalTable: "SmartPlaylistDefinitions",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Songs_SmartPlaylistDefinitions_SmartPlaylistDefinitionId1",
table: "Songs",
column: "SmartPlaylistDefinitionId1",
principalTable: "SmartPlaylistDefinitions",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Songs_Users_UserId",
table: "Songs",
column: "UserId",
principalTable: "Users",
principalColumn: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_SuggestionHelpers_SmartPlaylistDefinitions_SmartPlaylistDef~",
table: "SuggestionHelpers",
column: "SmartPlaylistDefinitionId",
principalTable: "SmartPlaylistDefinitions",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Songs_SmartPlaylistDefinitions_SmartPlaylistDefinitionId",
table: "Songs");
migrationBuilder.DropForeignKey(
name: "FK_Songs_SmartPlaylistDefinitions_SmartPlaylistDefinitionId1",
table: "Songs");
migrationBuilder.DropForeignKey(
name: "FK_Songs_Users_UserId",
table: "Songs");
migrationBuilder.DropForeignKey(
name: "FK_SuggestionHelpers_SmartPlaylistDefinitions_SmartPlaylistDef~",
table: "SuggestionHelpers");
migrationBuilder.DropTable(
name: "SmartPlaylistDefinitions");
migrationBuilder.DropIndex(
name: "IX_SuggestionHelpers_SmartPlaylistDefinitionId",
table: "SuggestionHelpers");
migrationBuilder.DropIndex(
name: "IX_Songs_SmartPlaylistDefinitionId",
table: "Songs");
migrationBuilder.DropIndex(
name: "IX_Songs_SmartPlaylistDefinitionId1",
table: "Songs");
migrationBuilder.DropIndex(
name: "IX_Songs_UserId",
table: "Songs");
migrationBuilder.DropColumn(
name: "SmartPlaylistDefinitionId",
table: "SuggestionHelpers");
migrationBuilder.DropColumn(
name: "SmartPlaylistDefinitionId",
table: "Songs");
migrationBuilder.DropColumn(
name: "SmartPlaylistDefinitionId1",
table: "Songs");
migrationBuilder.DropColumn(
name: "UserId",
table: "Songs");
}
}
}

View File

@@ -0,0 +1,282 @@
// <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("20250719222759_fix issues with Spotify session data for users")]
partial class fixissueswithSpotifysessiondataforusers
{
/// <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("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
b.HasIndex("UserId");
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
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<string>("SpotiyAuthAccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("SpotiyAuthCreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("SpotiyAuthExpiresAfterSeconds")
.HasColumnType("integer");
b.Property<string>("SpotiyAuthRefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
b.HasOne("User", null)
.WithMany("LikedSongs")
.HasForeignKey("UserId");
});
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");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
modelBuilder.Entity("User", b =>
{
b.Navigation("LikedSongs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class fixissueswithSpotifysessiondataforusers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SpotiyAuthAccessToken",
table: "Users",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<DateTime>(
name: "SpotiyAuthCreatedAt",
table: "Users",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "SpotiyAuthExpiresAfterSeconds",
table: "Users",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "SpotiyAuthRefreshToken",
table: "Users",
type: "text",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SpotiyAuthAccessToken",
table: "Users");
migrationBuilder.DropColumn(
name: "SpotiyAuthCreatedAt",
table: "Users");
migrationBuilder.DropColumn(
name: "SpotiyAuthExpiresAfterSeconds",
table: "Users");
migrationBuilder.DropColumn(
name: "SpotiyAuthRefreshToken",
table: "Users");
}
}
}

View File

@@ -0,0 +1,282 @@
// <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("20250719230009_some more fixes")]
partial class somemorefixes
{
/// <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("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
b.HasIndex("UserId");
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
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<string>("SpotifyAuthAccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("SpotifyAuthCreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("SpotifyAuthExpiresAfterSeconds")
.HasColumnType("integer");
b.Property<string>("SpotifyAuthRefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
b.HasOne("User", null)
.WithMany("LikedSongs")
.HasForeignKey("UserId");
});
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");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
modelBuilder.Entity("User", b =>
{
b.Navigation("LikedSongs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class somemorefixes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "SpotiyAuthRefreshToken",
table: "Users",
newName: "SpotifyAuthRefreshToken");
migrationBuilder.RenameColumn(
name: "SpotiyAuthExpiresAfterSeconds",
table: "Users",
newName: "SpotifyAuthExpiresAfterSeconds");
migrationBuilder.RenameColumn(
name: "SpotiyAuthCreatedAt",
table: "Users",
newName: "SpotifyAuthCreatedAt");
migrationBuilder.RenameColumn(
name: "SpotiyAuthAccessToken",
table: "Users",
newName: "SpotifyAuthAccessToken");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "SpotifyAuthRefreshToken",
table: "Users",
newName: "SpotiyAuthRefreshToken");
migrationBuilder.RenameColumn(
name: "SpotifyAuthExpiresAfterSeconds",
table: "Users",
newName: "SpotiyAuthExpiresAfterSeconds");
migrationBuilder.RenameColumn(
name: "SpotifyAuthCreatedAt",
table: "Users",
newName: "SpotiyAuthCreatedAt");
migrationBuilder.RenameColumn(
name: "SpotifyAuthAccessToken",
table: "Users",
newName: "SpotiyAuthAccessToken");
}
}
}

View File

@@ -0,0 +1,282 @@
// <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("20250719233912_explicitly specify keys")]
partial class explicitlyspecifykeys
{
/// <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("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
b.HasIndex("UserId");
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
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<string>("SpotifyAuthAccessToken")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("SpotifyAuthCreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("SpotifyAuthExpiresAfterSeconds")
.HasColumnType("integer");
b.Property<string>("SpotifyAuthRefreshToken")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
b.HasOne("User", null)
.WithMany("LikedSongs")
.HasForeignKey("UserId");
});
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");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
modelBuilder.Entity("User", b =>
{
b.Navigation("LikedSongs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class explicitlyspecifykeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -0,0 +1,280 @@
// <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("20250720003809_fix auth token non-null contraint")]
partial class fixauthtokennonnullcontraint
{
/// <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("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
b.HasIndex("UserId");
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
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<string>("SpotifyAuthAccessToken")
.HasColumnType("text");
b.Property<DateTime?>("SpotifyAuthCreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("SpotifyAuthExpiresAfterSeconds")
.HasColumnType("integer");
b.Property<string>("SpotifyAuthRefreshToken")
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
b.HasOne("User", null)
.WithMany("LikedSongs")
.HasForeignKey("UserId");
});
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");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
modelBuilder.Entity("User", b =>
{
b.Navigation("LikedSongs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class fixauthtokennonnullcontraint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SpotifyAuthRefreshToken",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<int>(
name: "SpotifyAuthExpiresAfterSeconds",
table: "Users",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<DateTime>(
name: "SpotifyAuthCreatedAt",
table: "Users",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<string>(
name: "SpotifyAuthAccessToken",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SpotifyAuthRefreshToken",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "SpotifyAuthExpiresAfterSeconds",
table: "Users",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "SpotifyAuthCreatedAt",
table: "Users",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "SpotifyAuthAccessToken",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
}
}
}

View File

@@ -0,0 +1,296 @@
// <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("20250720144512_explicitly model liked songs relationship")]
partial class explicitlymodellikedsongsrelationship
{
/// <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("LikedSongs", b =>
{
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("SongId", "UserId");
b.HasIndex("UserId");
b.ToTable("LikedSongs");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
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<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
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<string>("SpotifyAuthAccessToken")
.HasColumnType("text");
b.Property<DateTime?>("SpotifyAuthCreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("SpotifyAuthExpiresAfterSeconds")
.HasColumnType("integer");
b.Property<string>("SpotifyAuthRefreshToken")
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("LikedSongs", b =>
{
b.HasOne("Song", null)
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
});
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");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class explicitlymodellikedsongsrelationship : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Songs_Users_UserId",
table: "Songs");
migrationBuilder.DropIndex(
name: "IX_Songs_UserId",
table: "Songs");
migrationBuilder.DropColumn(
name: "UserId",
table: "Songs");
migrationBuilder.CreateTable(
name: "LikedSongs",
columns: table => new
{
SongId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LikedSongs", x => new { x.SongId, x.UserId });
table.ForeignKey(
name: "FK_LikedSongs_Songs_SongId",
column: x => x.SongId,
principalTable: "Songs",
principalColumn: "SongId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_LikedSongs_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_LikedSongs_UserId",
table: "LikedSongs",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LikedSongs");
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "Songs",
type: "integer",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Songs_UserId",
table: "Songs",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_Songs_Users_UserId",
table: "Songs",
column: "UserId",
principalTable: "Users",
principalColumn: "UserId");
}
}
}

View File

@@ -21,6 +21,54 @@ namespace song_of_the_day.DataMigrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LikedSongs", b =>
{
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("SongId", "UserId");
b.HasIndex("UserId");
b.ToTable("LikedSongs");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("CreatedByUserId")
.HasColumnType("integer");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IncludesLikedSongs")
.HasColumnType("boolean");
b.Property<bool>("IncludesUnCategorizedSongs")
.HasColumnType("boolean");
b.Property<string>("SpotifyPlaylistId")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.ToTable("SmartPlaylistDefinitions");
});
modelBuilder.Entity("Song", b =>
{
b.Property<int>("SongId")
@@ -35,11 +83,27 @@ namespace song_of_the_day.DataMigrations
b.Property<string>("Name")
.HasColumnType("text");
b.Property<int?>("Provider")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<int?>("SmartPlaylistDefinitionId1")
.HasColumnType("integer");
b.Property<string>("SpotifyId")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.HasKey("SongId");
b.HasIndex("SmartPlaylistDefinitionId");
b.HasIndex("SmartPlaylistDefinitionId1");
b.ToTable("Songs");
});
@@ -91,11 +155,16 @@ namespace song_of_the_day.DataMigrations
b.Property<string>("Description")
.HasColumnType("text");
b.Property<int?>("SmartPlaylistDefinitionId")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SmartPlaylistDefinitionId");
b.ToTable("SuggestionHelpers");
});
@@ -125,6 +194,18 @@ namespace song_of_the_day.DataMigrations
b.Property<string>("SignalMemberId")
.HasColumnType("text");
b.Property<string>("SpotifyAuthAccessToken")
.HasColumnType("text");
b.Property<DateTime?>("SpotifyAuthCreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("SpotifyAuthExpiresAfterSeconds")
.HasColumnType("integer");
b.Property<string>("SpotifyAuthRefreshToken")
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
@@ -133,6 +214,41 @@ namespace song_of_the_day.DataMigrations
b.ToTable("Users");
});
modelBuilder.Entity("LikedSongs", b =>
{
b.HasOne("Song", null)
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.HasOne("User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedByUserId");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Song", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyExcludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId");
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("ExplicitlyIncludedSongs")
.HasForeignKey("SmartPlaylistDefinitionId1");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
@@ -155,6 +271,22 @@ namespace song_of_the_day.DataMigrations
b.Navigation("User");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.HasOne("SmartPlaylistDefinition", null)
.WithMany("Categories")
.HasForeignKey("SmartPlaylistDefinitionId");
});
modelBuilder.Entity("SmartPlaylistDefinition", b =>
{
b.Navigation("Categories");
b.Navigation("ExplicitlyExcludedSongs");
b.Navigation("ExplicitlyIncludedSongs");
});
#pragma warning restore 612, 618
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
[PrimaryKey(nameof(Id))]
public class SmartPlaylistDefinition
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public User? CreatedBy { get; set; }
public bool IncludesUnCategorizedSongs { get; set; }
public bool IncludesLikedSongs { get; set; }
public List<SuggestionHelper>? Categories { get; set; }
public string? SpotifyPlaylistId { get; set; }
public List<Song>? ExplicitlyIncludedSongs { get; set; }
public List<Song>? ExplicitlyExcludedSongs { get; set; }
public bool IsThisUsersLikedSongsPlaylist
{
get
{
return IncludesLikedSongs == true
&& IncludesUnCategorizedSongs == false
&& (ExplicitlyExcludedSongs == null || ExplicitlyExcludedSongs.Count == 0)
&& (ExplicitlyIncludedSongs == null || ExplicitlyIncludedSongs.Count == 0)
&& (Categories == null || Categories.Count == 0);
}
}
}

View File

@@ -6,4 +6,8 @@ public class Song
public string? Name { get; set; }
public string? Artist { get; set; }
public string? Url { get; set; }
public SongProvider? Provider { get; set; }
public string? SpotifyId { get; set; }
public IList<User> LikedBy { get; set; }
}

View File

@@ -0,0 +1,11 @@
public enum SongProvider
{
Spotify,
YouTube,
YoutubeMusic,
SoundCloud,
Bandcamp,
PlainHttp,
NavidromeSharedLink,
Other
}

View File

@@ -10,4 +10,17 @@ public class User
public bool AssociationInProgress { get; set; }
public string? LdapUserName { get; set; }
public bool WasChosenForSuggestionThisRound { get; set; }
public string PreferredName
{
get
{
return (string.IsNullOrEmpty(NickName) ? Name : NickName).ToString();
}
}
public required List<Song> LikedSongs { get; set; }
public string? SpotifyAuthAccessToken { get; set; }
public int? SpotifyAuthExpiresAfterSeconds { get; set; }
public DateTime? SpotifyAuthCreatedAt { get; set; }
public string? SpotifyAuthRefreshToken { get; set; }
}

View File

@@ -0,0 +1,13 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~P:sotd.Pages.UnclaimedPhoneNumbersModel.userId")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~P:sotd.Pages.UserModel.userId")]
[assembly: SuppressMessage("Compiler", "CS8981:The type name only contains lower-cased ascii characters. Such names may become reserved for the language.", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.additionaldataforsongsubmissions")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.additionaldataforsongsubmissions")]
[assembly: SuppressMessage("Compiler", "CS8981:The type name only contains lower-cased ascii characters. Such names may become reserved for the language.", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.keeptrackofusersoickedforsubmission")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.keeptrackofusersoickedforsubmission")]

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,40 +1,61 @@
using System.Collections;
using System.ComponentModel;
using song_of_the_day;
public class LinkPreviewAttachment
{
public required string Url { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public required string Base64Image { get; set; }
}
public class SignalIntegration
{
public static SignalIntegration? Instance;
private readonly ILogger<SignalIntegration> logger;
public SignalIntegration(string uri, int port, string phoneNumber)
public SignalIntegration(ILogger<SignalIntegration> logger)
{
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)
BaseAddress = new Uri(uri + ":" + port),
Timeout = TimeSpan.FromSeconds(180),
};
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;
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);
try
{
ICollection<song_of_the_day.GroupEntry> groupEntries = await apiClient.GroupsAllAsync(this.phoneNumber);
foreach (var group in groupEntries)
{
Console.WriteLine($"{group.Name} {group.Id}");
logger.LogDebug($" {group.Name} {group.Id}");
}
}
catch (Exception ex)
{
Console.WriteLine("Exception (ListGroupsAsync): " + ex.Message);
logger.LogError("Exception (ListGroupsAsync): " + ex.Message);
}
}
@@ -45,14 +66,37 @@ 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);
}
catch (Exception ex)
{
Console.WriteLine("Exception (SendMessageToGroupAsync): " + ex.Message);
logger.LogError("Exception (SendMessageToGroupAsync): " + ex.Message);
}
}
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);
}
}
@@ -70,20 +114,48 @@ public class SignalIntegration
}
catch (Exception ex)
{
Console.WriteLine("Exception (SendMessageToUserAsync): " + ex.Message);
logger.LogError("Exception (SendMessageToUserAsync): " + ex.Message);
}
}
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)
{
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);
await this.SendMessageToUserAsync("In that community group I will pick a person at random every now and then and encourage them to share a song with the rest of the community.", user.SignalMemberId);
if (AppConfiguration.Instance.UseBotTag)
{
await this.SendMessageToUserAsync("You can always see which messages are sent by me rather than the community host by the **[Proggy]** tag at the beginning of the message", user.SignalMemberId);
}
await this.SendMessageToUserAsync($"Not right now, but eventually you will be able to see more details about your community at {AppConfiguration.Instance.WebUIBaseURL}.", user.SignalMemberId);
await this.SendMessageToUserAsync($"You can find more details about your community at {AppConfiguration.Instance.WebUIBaseURL}.", user.SignalMemberId);
await this.SendMessageToUserAsync($"""You can navigate to {AppConfiguration.Instance.WebUIBaseURL + (AppConfiguration.Instance.WebUIBaseURL.EndsWith("/") ? "" : "/")}User/{user.UserId} to set your preferred display name for me to use.""", user.SignalMemberId);
await this.SendMessageToUserAsync($"Now have fun and enjoy being a part of this community!", user.SignalMemberId);
}
@@ -97,7 +169,7 @@ public class SignalIntegration
}
catch (Exception ex)
{
Console.WriteLine("Exception (GetMemberListAsync): " + ex.Message);
logger.LogError("Exception (GetMemberListAsync): " + ex.Message);
}
return new List<string>();
}
@@ -117,7 +189,7 @@ public class SignalIntegration
}
catch (Exception ex)
{
Console.WriteLine("Exception (GetContactAsync): " + ex.Message);
logger.LogError("Exception (GetContactAsync): " + ex.Message);
return new ListContactsResponse();
}
}

View File

@@ -5,6 +5,47 @@
}
<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>
@if(@User.Identity.IsAuthenticated)
{
<th>Details</th>
<th></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>
@if(@User.Identity.IsAuthenticated)
{
<td><a href="/SongSubmission/@songSuggestion?.Id">View</a></td>
<td class="=viewLike">
@if(songSuggestion?.Song != null)
{
var handler = Model.HasUserLikedThisSong(@songSuggestion.Song) ? "UnlikeSong" : "LikeSong";
var likebuttonText = Model.HasUserLikedThisSong(@songSuggestion.Song) ? "Unlike" : "Like";
<form method="post">
<input name="songId" value="@songSuggestion.Song.SongId" type="hidden" />
<input type="submit" id="songlike" value="@likebuttonText" asp-page-handler="@handler" />
</form>
}
</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,71 @@ public class IndexModel : PageModel
_logger = logger;
}
public void OnGet()
{
[BindProperty]
public List<SongSuggestion> SongSuggestions { get; set; } = new List<SongSuggestion>();
private User _currentUser;
public User CurrentUser
{
get
{
if (_currentUser == null)
{
var userName = this.User.Identity.Name;
using (var dci = DataContext.Instance)
{
_currentUser = dci.Users.Include(u => u.LikedSongs).Where(u => u.LdapUserName == userName).SingleOrDefault();
}
}
return _currentUser;
}
}
public bool HasUserLikedThisSong(Song song)
{
return CurrentUser.LikedSongs.Where(s => s.SongId == song.SongId).FirstOrDefault() != default(Song);
}
public Task OnGet()
{
using var dci = DataContext.Instance;
SongSuggestions = dci.SongSuggestions.OrderByDescending(s => s.Date)
.Take(50)
.Include(s => s.Song)
.Include(s => s.User)
.ToList();
return Task.CompletedTask;
}
public async Task<IActionResult> OnPostLikeSong(int? songId)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Include(u => u.LikedSongs).Where(u => u.UserId == CurrentUser.UserId).SingleOrDefault();
if (user.LikedSongs.Find(s => s.SongId == songId) == default(Song))
{
user.LikedSongs.Add(dci.Songs.Find(songId));
await dci.SaveChangesAsync();
}
}
return RedirectToPage("/");
}
public async Task<IActionResult> OnPostUnlikeSong(int? songId)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Include(u => u.LikedSongs).Where(u => u.UserId == CurrentUser.UserId).SingleOrDefault();
var songToRemove = user.LikedSongs.Where(s => s.SongId == songId).SingleOrDefault();
if (songToRemove != default(Song))
{
user.LikedSongs.Remove(songToRemove);
await dci.SaveChangesAsync();
}
}
return RedirectToPage("/");
}
}

View File

@@ -0,0 +1,16 @@
public class SongPartialModel
{
public required 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

@@ -0,0 +1,2 @@
@inherits Microsoft.AspNetCore.Components.Forms.InputText
<input @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />

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,12 +37,20 @@
<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 != null && this.User.Identity != null &&
this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
{
<li class="nav-item">
<a class="nav-link text-dark" href="/SongSubmission/">Song Submissions</a>
</li>
}
</ul>
</div>
</div>

View File

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

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>
@@ -23,9 +23,15 @@
}
else
{
var userName = User.Identity.Name;
var dci = DataContext.Instance;
var selectedUsers = dci.Users.Where(u => u.LdapUserName == userName);
var userId = selectedUsers.SingleOrDefault()?.UserId;
dci.Dispose();
<form method="post" action="Auth/Logout">
<div>
Welcome, @User.Identity.Name!
Welcome, <a href="/User/@userId">@User.Identity.Name</a>!
</div>
<div>
<input name="submit" type="submit" value="Logout" />

View File

@@ -0,0 +1,8 @@
@model SongPartialModel
<label asp-for="Name">Name:</label>
<input asp-for="Name" oninput="UpdateSongSuggestions()" disabled="@Model.IsPageReadonly" />
<label asp-for="Artist">Artist:</label>
<input asp-for="Artist" oninput="UpdateSongSuggestions()" disabled="@Model.IsPageReadonly" />
<label asp-for="SpotifyId">Spotify ID:</label>
<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,170 @@
@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>
<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>
<td class="=viewLike">
@if(submission.Song != null)
{
var handler = Model.HasUserLikedThisSong(submission.Song) ? "UnlikeSong" : "LikeSong";
var likebuttonText = Model.HasUserLikedThisSong(submission.Song) ? "Unlike" : "Like";
<form method="post">
<input name="songId" value="@submission.Song.SongId" type="hidden" />
<input type="submit" id="songlike" value="@likebuttonText" asp-page-handler="@handler" />
</form>
}
</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,290 @@
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;
}
}
}
private User _currentUser;
public User CurrentUser
{
get
{
if (_currentUser == null)
{
var userName = this.User.Identity.Name;
using (var dci = DataContext.Instance)
{
_currentUser = dci.Users.Include(u => u.LikedSongs).Where(u => u.LdapUserName == userName).SingleOrDefault();
}
}
return _currentUser;
}
}
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;
public bool HasUserLikedThisSong(Song song)
{
return CurrentUser.LikedSongs.Where(s => s.SongId == song.SongId).FirstOrDefault() != default(Song);
}
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}** \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 async Task<IActionResult> OnPostLikeSong(int? songId)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Include(u => u.LikedSongs).Where(u => u.UserId == CurrentUser.UserId).SingleOrDefault();
if (user.LikedSongs.Find(s => s.SongId == songId) == default(Song))
{
user.LikedSongs.Add(dci.Songs.Find(songId));
await dci.SaveChangesAsync();
}
}
return RedirectToPage("SongSubmission");
}
public async Task<IActionResult> OnPostUnlikeSong(int? songId)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Include(u => u.LikedSongs).Where(u => u.UserId == CurrentUser.UserId).SingleOrDefault();
var songToRemove = user.LikedSongs.Where(s => s.SongId == songId).SingleOrDefault();
if (songToRemove != default(Song))
{
user.LikedSongs.Remove(songToRemove);
await dci.SaveChangesAsync();
}
}
return RedirectToPage("SongSubmission");
}
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

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

@@ -1,4 +1,5 @@
@page "{userIndex}"
@model UserModel
@{
ViewData["Title"] = "User #" + @Model.userId;
@@ -12,6 +13,20 @@
<label asp-for="UserName">Contact Name</label>
<input asp-for="UserName" disabled />
<br />
<input type="submit" title="Submit" />
<input type="submit" value="Submit" />
</form>
@if(@Model.IsSpotifyAuthenticated)
{
<form method="post" >
<input name="userIndex" value="@Model.userId" type="hidden" />
<input type="submit" value="Deauthorize Spotify" asp-page-handler="SpotifyLogout" />
</form>
}
else
{
<form method="post" >
<input name="userIndex" value="@Model.userId" type="hidden" />
<input type="submit" value="Connect to Spotify" asp-page-handler="SpotifyLogin" />
</form>
}
</div>

View File

@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using SpotifyAPI.Web;
using System.Web;
namespace sotd.Pages;
@@ -18,28 +21,67 @@ public class UserModel : PageModel
public string UserName { get; set; }
public bool IsSpotifyAuthenticated { get; set; }
[BindProperty]
public string UserNickName { get; set; }
public void OnGet(int userIndex)
public async Task OnGet(int userIndex, [FromServices] SpotifyApiClient spotifyClient)
{
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;
this.IsSpotifyAuthenticated = await spotifyClient.IsUserAuthenticatedAsync(user);
}
}
public void OnPost(int userIndex)
public async Task OnPost(int userIndex, [FromServices] SpotifyApiClient spotifyClient)
{
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;
this.IsSpotifyAuthenticated = await spotifyClient.IsUserAuthenticatedAsync(user);
}
}
}
public IActionResult OnPostSpotifyLogin(int userIndex, string currentUri, [FromServices] SpotifyApiClient spotifyClient)
{
_logger.LogTrace($"Attempting Spotify login for user with id {userIndex}.");
var loginRequest = new LoginRequest(
new Uri(spotifyClient.GetLoginRedirectUri()),
AppConfiguration.Instance.SpotifyClientId,
LoginRequest.ResponseType.Code)
{
Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative, Scopes.PlaylistModifyPrivate, Scopes.PlaylistModifyPublic, Scopes.UgcImageUpload }
};
var redirectUri = loginRequest.ToUri().ToString() + $"&finalRedirect={HttpUtility.UrlEncode($"{Request.Scheme}://{Request.Host}:{Request.Host.Port ?? 80}{Request.Path}")}";
return this.Redirect(redirectUri);
}
public async Task OnPostSpotifyLogout(int userIndex, [FromServices] SpotifyApiClient spotifyClient)
{
_logger.LogTrace($"Attempting to deauthorize Spotify for user with id {userIndex}.");
using (var dci = DataContext.Instance)
{
var user = dci.Users?.Find(userIndex);
if (user != null)
{
await spotifyClient.DeAuthorizeUserAsync(user);
user.NickName = this.UserNickName;
this.UserName = user.Name ?? string.Empty;
}
}
}
}

View File

@@ -1,28 +1,42 @@

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);
using Microsoft.AspNetCore.Mvc;
using SpotifyAPI.Web;
using Microsoft.AspNetCore.Components.Authorization;
var builder = WebApplication.CreateBuilder(args);
Console.WriteLine("Setting up user check timer");
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
// 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<SpotifyApiClient>();
builder.Services.AddSingleton<PlayListSynchronizer>();
builder.Services.AddSingleton<SongResolver>();
var app = builder.Build();
var logger = app.Logger;
logger.LogTrace("Setting up user check timer");
var userCheckTimer = new CronTimer(AppConfiguration.Instance.UserCheckTimerSchedule, "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)
@@ -30,10 +44,10 @@ 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);
Console.WriteLine("New user:");
Console.WriteLine($" Name: {newUserContact.Name}");
Console.WriteLine($" MemberId: {memberId}");
var newUserContact = await signalIntegration.GetContactAsync(memberId);
logger.LogDebug("New user:");
logger.LogDebug($" Name: {newUserContact.Name}");
logger.LogDebug($" MemberId: {memberId}");
User newUser = new User()
{
Name = newUserContact.Name,
@@ -43,6 +57,7 @@ userCheckTimer.OnOccurence += async (s, ea) =>
LdapUserName = string.Empty,
AssociationInProgress = false,
WasChosenForSuggestionThisRound = false,
LikedSongs = new List<Song>()
};
dci.Users?.Add(newUser);
needsSaving = true;
@@ -55,10 +70,49 @@ userCheckTimer.OnOccurence += async (s, ea) =>
}
await dci.DisposeAsync();
};
userCheckTimer.Start();
Console.WriteLine("Setting up user intro timer");
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
logger.LogTrace("Setting up liked songs playlist creation timer");
var likePlaylistCheckTimer = new CronTimer(AppConfiguration.Instance.LikePlaylistCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
likePlaylistCheckTimer.OnOccurence += async (s, ea) =>
{
var spotifyApiClient = app.Services.GetService<SpotifyApiClient>();
var playlistSynchronizer = app.Services.GetService<PlayListSynchronizer>();
var dci = DataContext.Instance;
var needsSaving = false;
var allUsers = dci.Users.ToList();
await dci.DisposeAsync();
dci = DataContext.Instance;
foreach (var user in allUsers)
{
if (!await spotifyApiClient.IsUserAuthenticatedAsync(user))
{
logger.LogWarning($"User {user.LdapUserName} is not authorized with Spotify - skipping playlist sync");
continue;
}
var foundPlaylist = dci.SmartPlaylistDefinitions?.Where(p => p.CreatedBy == user).ToList().Where(p => p.IsThisUsersLikedSongsPlaylist).SingleOrDefault();
if (foundPlaylist == null)
{
logger.LogWarning($"Creating liked songs playlist for user {user.LdapUserName}");
var title = $"{user.PreferredName}'s liked songs";
var description = $"A collection of the songs liked by {user.PreferredName} on their 'Song of the day' server instance.";
// playlist does not exist yet, creating it
var newPlaylist = await (await spotifyApiClient.WithUserAuthorizationAsync(user)).CreateSpotifyPlaylist(title, description, false, true, user);
await playlistSynchronizer.SynchronizePlaylistAsync(newPlaylist);
needsSaving = true;
}
logger.LogWarning($"Syncing playlists for user {user.LdapUserName}");
await playlistSynchronizer.SynchronizeUserPlaylistsAsync(user);
}
if (needsSaving)
{
await dci.SaveChangesAsync();
}
await dci.DisposeAsync();
};
logger.LogTrace("Setting up user intro timer");
var userIntroTimer = new CronTimer(AppConfiguration.Instance.UserIntroCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
userIntroTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
@@ -72,7 +126,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;
}
@@ -83,43 +138,42 @@ userIntroTimer.OnOccurence += async (s, ea) =>
}
await dci.DisposeAsync();
};
userIntroTimer.Start();
Console.WriteLine("Setting up pick of the day timer");
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
logger.LogTrace("Setting up pick of the day timer");
var pickOfTheDayTimer = new CronTimer(AppConfiguration.Instance.PickOfTheDayCheckTimerSchedule, "Europe/Vienna", includingSeconds: false);
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
var lastSong = dci.SongSuggestions?.LastOrDefault();
var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();
if (lastSong != null && lastSong.Date >= DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
if (lastSong != null && lastSong.Date > DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
{
Console.WriteLine("Skipping pick of the day today!");
logger.LogWarning("Skipping pick of the day today!");
await dci.DisposeAsync();
return;
}
if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
{
Console.WriteLine("Unable to properly initialize DB context!");
logger.LogError("Unable to properly initialize DB context!");
await dci.DisposeAsync();
return;
}
var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
if (!potentialUsers.Any())
{
Console.WriteLine("Resetting suggestion count on users before resuming");
logger.LogTrace("Resetting suggestion count on users before resuming");
await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
await dci.SaveChangesAsync();
potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
}
Console.WriteLine("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
logger.LogDebug("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
if (luckyUser == null)
{
Console.WriteLine("Unable to determine today's lucky user!");
logger.LogError("Unable to determine today's lucky user!");
await dci.DisposeAsync();
return;
}
@@ -131,41 +185,43 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
SuggestionHelper = suggestion,
UserHasSubmitted = false,
HasUsedSuggestion = false,
Date = DateTime.Today
Date = DateTime.Today.ToUniversalTime()
};
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();
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.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($"Please navigate to https://sotd.disi.dev/SongSubmission/{newSongSuggestion.Id} to submit your choice!", luckyUser.SignalMemberId);
}
await dci.DisposeAsync();
};
pickOfTheDayTimer.Start();
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);
}
};
};
Console.WriteLine("Setting up LdapAssociation timer");
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
logger.LogTrace("Setting up LdapAssociation timer");
var ldapAssociationTimer = new CronTimer(AppConfiguration.Instance.LdapAssociationTimerSchedule, "Europe/Vienna", includingSeconds: false);
ldapAssociationTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
if (dci.Users == null)
{
Console.WriteLine("Unable to properly initialize DB context!");
logger.LogError("Unable to properly initialize DB context!");
await dci.DisposeAsync();
return;
}
@@ -186,27 +242,32 @@ ldapAssociationTimer.OnOccurence += async (s, ea) =>
}
await dci.DisposeAsync();
};
ldapAssociationTimer.Start();
var searchResults = LdapIntegration.Instance.SearchInAD(
AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
$"(memberOf={AppConfiguration.Instance.LDAPConfig.CrewGroup})",
SearchScope.Subtree
);
logger.LogTrace("Setting up MessageSync timer");
var messageSyncTimer = new CronTimer(AppConfiguration.Instance.MessageSyncTimerSchedule, "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();
// 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>();
var app = builder.Build();
// only start interaction timers in production builds
// for local/development testing we want those disabled
likePlaylistCheckTimer.Start();
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())
@@ -234,6 +295,27 @@ app.MapControllerRoute(
name: "logout",
pattern: "{controller=Auth}/{action=Logout}"
);
app.MapGet("SpotifyLogin", async (HttpRequest request, HttpResponse response) =>
{
var spotifyClient = app.Services.GetService<SpotifyApiClient>();
var code = request.Query["code"];
var oAuthResponse = await new OAuthClient().RequestToken(
new AuthorizationCodeTokenRequest(AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret, code, new Uri(spotifyClient.GetLoginRedirectUri()))
);
var dci = DataContext.Instance;
var userName = request.HttpContext.User.Identity.Name;
var loggedInUser = dci.Users.Where(u => u.LdapUserName == userName).SingleOrDefault();
var userId = loggedInUser.UserId;
loggedInUser.SpotifyAuthAccessToken = oAuthResponse.AccessToken;
loggedInUser.SpotifyAuthExpiresAfterSeconds = oAuthResponse.ExpiresIn;
loggedInUser.SpotifyAuthCreatedAt = oAuthResponse.CreatedAt;
loggedInUser.SpotifyAuthRefreshToken = oAuthResponse.RefreshToken;
await dci.SaveChangesAsync();
dci.Dispose();
response.Redirect($"/User/{userId}");
});
app.MapGet("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
string.Join("\n", endpointSources.SelectMany(source => source.Endpoints)));

View File

@@ -0,0 +1,31 @@
public class Base64UrlImageBuilder
{
public required 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 = string.Empty;
public string FileContents
{
get
{
return _fileContents;
}
}
public override string ToString()
{
//return $"data:{ContentType};base64,{FileContents}";
return $"{FileContents}";
}
}

View File

@@ -0,0 +1,10 @@
public interface ISongValidator
{
Task<Song> ValidateAsync(Uri songUri);
Task<bool> CanValidateUriAsync(Uri songUri);
Task<bool> CanExtractSongMetadataAsync(Uri songUri);
SongProvider GetSongProvider();
}

View File

@@ -0,0 +1,121 @@
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using YouTubeMusicAPI.Client;
using System.Text.Json;
using Acornima.Ast;
using AngleSharp.Text;
using System.Text.RegularExpressions;
using System.Text.Json.Serialization;
public class NavidromeValidator : SongValidatorBase
{
private YouTubeMusicClient youtubeClient;
public NavidromeValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
{
youtubeClient = new("AT");
}
public override async Task<bool> CanValidateUriAsync(Uri songUri)
{
// Check if behind this URL there is a public navidrome share
var config = Configuration.Default.WithDefaultLoader();
var address = songUri.ToString();
var context = BrowsingContext.New(config);
var document = await context.OpenAsync(address);
var titleCell = (document.DocumentElement.GetDescendants().First() as HtmlElement)
.GetElementsByTagName("title").First();
return "Navidrome".Equals(titleCell.TextContent);
}
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
return await this.CanValidateUriAsync(songUri);
}
public override SongProvider GetSongProvider()
{
return SongProvider.NavidromeSharedLink;
}
private static Stream GenerateStreamFromString(string s)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(s);
writer.Flush();
stream.Position = 0;
return stream;
}
public override async Task<Song> ValidateAsync(Uri songUri)
{
var config = Configuration.Default.WithDefaultLoader();
var address = songUri.ToString();
var context = BrowsingContext.New(config);
var document = await context.OpenAsync(address);
var infoScriptNode = document.GetElementsByTagName("script").Where(e => e.TextContent.Contains("__SHARE_INFO__")).First();
var manipulatedValue = infoScriptNode.TextContent.Replace("window.__SHARE_INFO__ = \"", "").StripLeadingTrailingSpaces().StripLineBreaks();
manipulatedValue = manipulatedValue.Remove(manipulatedValue.Length - 1);
var infoScriptJsonData = Regex.Unescape(Regex.Unescape(manipulatedValue));
var title = string.Empty;
var artist = string.Empty;
using (var stream = GenerateStreamFromString(infoScriptJsonData))
{
var jsonContent = await JsonSerializer.DeserializeAsync<NavidromeShareInfoData>(stream);
title = jsonContent.Tracks[0].Title;
artist = jsonContent.Tracks[0].Artist;
}
var song = new Song
{
Name = title,
Artist = artist,
Url = songUri.ToString(),
Provider = SongProvider.NavidromeSharedLink,
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
};
return song;
}
}
public class NavidromeShareInfoData
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("description")]
public required string Description { get; set; }
[JsonPropertyName("downloadable")]
public bool Downloadable { get; set; }
[JsonPropertyName("tracks")]
public required List<NavidromeTrackInfoData> Tracks { get; set; }
}
public class NavidromeTrackInfoData
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("title")]
public required string Title { get; set; }
[JsonPropertyName("artist")]
public required string Artist { get; set; }
[JsonPropertyName("album")]
public required string Album { get; set; }
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; }
[JsonPropertyName("duration")]
public float Duration { get; set; }
}

View File

@@ -0,0 +1,85 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
public class SongResolver
{
private readonly IEnumerable<ISongValidator> _songValidators;
private readonly 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 => { return (mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.Split("`")[0].EndsWith("Base"))); }))
{
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);
}
}
}
public async Task<Song> ResolveSongAsync(Uri songUri)
{
foreach (var validator in _songValidators)
{
if (await validator.CanValidateUriAsync(songUri))
{
if (!await validator.CanExtractSongMetadataAsync(songUri))
{
this.logger.LogWarning("Cannot extract metadata for song URI: {SongUri}", songUri);
return new Song
{
Artist = "Unknown Artist",
Name = "Unknown Title",
Url = songUri.ToString(),
Provider = SongProvider.PlainHttp,
};
}
return await validator.ValidateAsync(songUri);
}
}
return new Song
{
Artist = "Unknown Artist",
Name = "Unknown Title",
Url = songUri.ToString(),
Provider = SongProvider.PlainHttp,
};
}
public bool CanValidate(Uri songUri)
{
foreach (var validator in _songValidators)
{
if (validator.CanValidateUriAsync(songUri).Result)
{
return true;
}
}
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"
};
}
}

View File

@@ -0,0 +1,30 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public abstract class SongValidatorBase : ISongValidator
{
public abstract Task<Song> ValidateAsync(Uri songUri);
public abstract Task<bool> CanExtractSongMetadataAsync(Uri songUri);
public abstract Task<bool> CanValidateUriAsync(Uri songUri);
public abstract SongProvider GetSongProvider();
protected SpotifyApiClient _spotifyApiClient;
protected ILogger _logger;
public SongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient)
{
_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 : "";
}
}

View File

@@ -0,0 +1,41 @@
using System.Text.RegularExpressions;
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
public class SpotifyValidator : UriBasedSongValidatorBase
{
public override string UriValidatorRegex => @"^(https?://)?open.spotify.com/track/([a-zA-Z0-9_-]{22})(\?si=[a-zA-Z0-9_-]+)?$";
public SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient)
{ }
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
return await this.CanValidateUriAsync(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 song = new Song
{
Name = track.Name,
Artist = track.Artists.FirstOrDefault()?.Name ?? "Unknown Artist",
Url = songUri.ToString(),
Provider = SongProvider.Spotify,
SpotifyId = trackIdMatch
};
return song;
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.RegularExpressions;
public abstract class UriBasedSongValidatorBase : SongValidatorBase
{
public abstract string UriValidatorRegex { get; }
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());
}
public override async Task<bool> CanValidateUriAsync(Uri songUri)
{
var result = await Task.Run(() =>
{
return GetUriMatch(songUri).Success;
});
return result;
}
}

View File

@@ -0,0 +1,48 @@
using AngleSharp;
using AngleSharp.Dom;
using YouTubeMusicAPI.Client;
public class YoutubeMusicValidator : UriBasedSongValidatorBase
{
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 async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{
return await this.CanValidateUriAsync(songUri);
}
public override async Task<Song> ValidateAsync(Uri songUri)
{
var match = this.GetUriMatch(songUri);
var playlistId = match.Groups[3].Value;
var playlistResult = await youtubeClient.GetCommunityPlaylistInfoAsync(playlistId);
var songData = playlistResult.Songs[0];
var title = songData.Name;
var artist = songData.Artists[0].Name;
var song = new Song
{
Name = title,
Artist = artist,
Url = songUri.ToString(),
Provider = SongProvider.YoutubeMusic,
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
};
return song;
}
}

View File

@@ -0,0 +1,48 @@
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)
{
return await this.CanValidateUriAsync(songUri);
}
public override SongProvider GetSongProvider()
{
return SongProvider.YouTube;
}
public override async Task<Song> ValidateAsync(Uri songUri)
{
var match = this.GetUriMatch(songUri);
var songId = match.Groups[4].Value;
var songData = await youtubeClient.GetSongVideoInfoAsync(songId);
var title = songData.Name;
var artist = songData.Artists[0].Name;
var song = new Song
{
Name = title,
Artist = artist,
Url = songUri.ToString(),
Provider = SongProvider.YouTube,
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
};
return song;
}
}

View File

@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore;
public class PlayListSynchronizer
{
private SpotifyApiClient _spotifyAPIClient;
public PlayListSynchronizer(SpotifyApiClient spotifyAPIClient)
{
_spotifyAPIClient = spotifyAPIClient;
}
public async Task SynchronizePlaylistAsync(SmartPlaylistDefinition playlist)
{
var songsToInclude = new List<Song>();
using (var dci = DataContext.Instance)
{
songsToInclude.AddRange(dci.SongSuggestions.Where(ss => ss.HasUsedSuggestion && playlist.Categories.Contains(ss.SuggestionHelper) && !songsToInclude.Contains(ss.Song)).Select(ss => ss.Song));
songsToInclude.AddRange(playlist.ExplicitlyIncludedSongs.Where(ss => !songsToInclude.Contains(ss)));
if (playlist.IncludesUnCategorizedSongs)
{
songsToInclude.AddRange(dci.SongSuggestions.Where(ss => !ss.HasUsedSuggestion && !songsToInclude.Contains(ss.Song)).Select(ss => ss.Song));
}
if (playlist.IncludesLikedSongs)
{
var userWithLikes = dci.Users.Include(u => u.LikedSongs).Where(u => u.UserId == playlist.CreatedBy.UserId).SingleOrDefault();
var likedSongs = userWithLikes.LikedSongs;
songsToInclude.AddRange(likedSongs.Where(s => !songsToInclude.Contains(s)));
}
}
songsToInclude.RemoveAll(song => playlist.ExplicitlyExcludedSongs.Contains(song));
var spotifyIdsToInclude = songsToInclude.Select(s => s.SpotifyId);
var apiClient = await _spotifyAPIClient.WithUserAuthorizationAsync(playlist.CreatedBy);
var songsAlreadyInPlaylist = await apiClient.GetSongsInPlaylist(playlist.SpotifyPlaylistId);
var songsToAdd = spotifyIdsToInclude.Where(sti => !songsAlreadyInPlaylist.Contains(sti)).ToList();
var songsToRemove = songsAlreadyInPlaylist.Where(sai => !spotifyIdsToInclude.Contains(sai)).ToList();
apiClient.AddSongsToPlaylist(playlist.SpotifyPlaylistId, songsToAdd);
apiClient.RemoveSongsFromPlaylist(playlist.SpotifyPlaylistId, songsToRemove);
}
public async Task SynchronizePlaylistsAsync(IList<SmartPlaylistDefinition> playlists)
{
foreach (var playlist in playlists)
{
await SynchronizePlaylistAsync(playlist);
}
}
public async Task SynchronizeUserPlaylistsAsync(User user)
{
using (var dci = DataContext.Instance)
{
var userPlayLists = dci.SmartPlaylistDefinitions
.Include(pl => pl.ExplicitlyIncludedSongs)
.Include(pl => pl.ExplicitlyExcludedSongs)
.Include(pl => pl.Categories)
.Include(pl => pl.CreatedBy)
.Where(pl => pl.CreatedBy == user).ToList();
await SynchronizePlaylistsAsync(userPlayLists);
}
}
}

View File

@@ -0,0 +1,290 @@
using SpotifyAPI.Web;
using System.Web;
using Microsoft.EntityFrameworkCore;
public class SpotifyApiClient
{
private SpotifyClient _spotifyClient;
private SpotifyClient? _userAuthorizedSpotifyClient;
private ILogger<SpotifyApiClient> _logger;
public SpotifyApiClient(ILogger<SpotifyApiClient> logger)
{
var config = SpotifyClientConfig.CreateDefault()
.WithAuthenticator(new ClientCredentialsAuthenticator(
AppConfiguration.Instance.SpotifyClientId,
AppConfiguration.Instance.SpotifyClientSecret));
_spotifyClient = new SpotifyClient(config);
_userAuthorizedSpotifyClient = null;
_logger = logger;
}
public async Task<SpotifyApiClient> WithUserAuthorizationAsync(User user)
{
var refreshResponse = await new OAuthClient().RequestToken(
new AuthorizationCodeRefreshRequest(
AppConfiguration.Instance.SpotifyClientId,
AppConfiguration.Instance.SpotifyClientSecret,
user.SpotifyAuthRefreshToken)
);
var config = SpotifyClientConfig
.CreateDefault()
.WithAuthenticator(new AuthorizationCodeAuthenticator(
AppConfiguration.Instance.SpotifyClientId,
AppConfiguration.Instance.SpotifyClientSecret,
new AuthorizationCodeTokenResponse()
{
RefreshToken = refreshResponse.RefreshToken,
AccessToken = refreshResponse.AccessToken,
TokenType = refreshResponse.TokenType,
ExpiresIn = refreshResponse.ExpiresIn,
Scope = refreshResponse.Scope,
CreatedAt = refreshResponse.CreatedAt
}));
_userAuthorizedSpotifyClient = new SpotifyClient(config);
return this;
}
private SpotifyClient UserAuthorizedSpotifyClient
{
get
{
if (_userAuthorizedSpotifyClient == null)
{
throw new Exception("Cannot perform Spotify API call without user authorization. Authorize Spotify access from your user page first!");
}
return _userAuthorizedSpotifyClient;
}
}
public async Task<List<FullTrack>> GetTrackCandidatesAsync(string trackName, string artistName)
{
try
{
var searchResponse = await _spotifyClient.Search.Item(new SearchRequest(SearchRequest.Types.Track, $"{trackName} {artistName}")
{
Limit = 5
});
return searchResponse.Tracks.Items ?? new List<FullTrack>();
}
catch (APIException ex)
{
throw new Exception($"Error fetching tracks by query: \"{trackName} {artistName}\": {ex.Message}", ex);
}
}
public string GetLoginRedirectUri()
{
return AppConfiguration.Instance.WebUIBaseURL + (AppConfiguration.Instance.WebUIBaseURL.EndsWith("/") ? "SpotifyLogin" : "/SpotifyLogin");
}
private bool IsAuthTokenExpired(User user)
{
if (user.SpotifyAuthCreatedAt == null || user.SpotifyAuthExpiresAfterSeconds == null)
{
return true;
}
var expirationdate = user.SpotifyAuthCreatedAt.Value.AddSeconds(user.SpotifyAuthExpiresAfterSeconds.Value);
return expirationdate < DateTime.UtcNow;
}
public async Task DeAuthorizeUserAsync(User user)
{
using (var dci = DataContext.Instance)
{
var isEntityTracked = dci.Entry(user).State != EntityState.Detached;
if (!isEntityTracked)
{
user = dci.Users.Find(user.UserId);
}
user.SpotifyAuthAccessToken = string.Empty;
user.SpotifyAuthRefreshToken = string.Empty;
user.SpotifyAuthExpiresAfterSeconds = null;
user.SpotifyAuthCreatedAt = null;
await dci.SaveChangesAsync();
}
}
public async Task<string> GetValidAuthorizationTokenAsync(User user)
{
if (string.IsNullOrEmpty(user.SpotifyAuthAccessToken) || string.IsNullOrEmpty(user.SpotifyAuthRefreshToken))
{
// user either never connected Spotify or we failed to refresh token - user needs to re-authenticate
return string.Empty;
}
if (!this.IsAuthTokenExpired(user))
{
return user.SpotifyAuthAccessToken;
}
// if token is expired, attempt a refresh
var dci = DataContext.Instance;
var isEntityTracked = dci.Entry(user).State != EntityState.Detached;
if (!isEntityTracked)
{
user = dci.Users.Find(user.UserId);
}
try
{
var oAuthResponse = await new OAuthClient().RequestToken(
new AuthorizationCodeRefreshRequest(AppConfiguration.Instance.SpotifyClientId, AppConfiguration.Instance.SpotifyClientSecret, user.SpotifyAuthRefreshToken)
);
user.SpotifyAuthAccessToken = oAuthResponse.AccessToken;
user.SpotifyAuthExpiresAfterSeconds = oAuthResponse.ExpiresIn;
user.SpotifyAuthCreatedAt = oAuthResponse.CreatedAt;
user.SpotifyAuthRefreshToken = oAuthResponse.RefreshToken;
return user.SpotifyAuthAccessToken;
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to refresh SpotifyAuth token for user {user.LdapUserName}: {ex.Message}");
await DeAuthorizeUserAsync(user);
return string.Empty;
}
finally
{
await dci.SaveChangesAsync();
dci.Dispose();
}
}
public async Task<bool> IsUserAuthenticatedAsync(User user)
{
return !string.IsNullOrEmpty(await this.GetValidAuthorizationTokenAsync(user));
}
public async Task<FullTrack> GetTrackByIdAsync(string trackId)
{
try
{
return await _spotifyClient.Tracks.Get(trackId);
}
catch (APIException ex)
{
throw new Exception($"Error fetching track by ID: {trackId}: {ex.Message}", ex);
}
}
public async Task<SmartPlaylistDefinition> CreateSpotifyPlaylist(string playlistTitle,
string description,
bool IncludesUnCategorizedSongs,
bool IncludesLikedSongs,
User createdBy)
{
try
{
// for now hardcoded with my user ID
var playlistCreationRequest = new PlaylistCreateRequest(playlistTitle);
playlistCreationRequest.Public = true;
playlistCreationRequest.Collaborative = false;
playlistCreationRequest.Description = description;
var currentUser = await UserAuthorizedSpotifyClient.UserProfile.Current();
var playlist = await UserAuthorizedSpotifyClient.Playlists.Create(currentUser.Id, playlistCreationRequest);
_logger.LogWarning($"Creating new playlist '{playlistTitle}'");
using (var dci = DataContext.Instance)
{
var trackedUserEntity = dci.Users.Find(createdBy.UserId);
var newPlaylist = new SmartPlaylistDefinition()
{
Title = playlistTitle,
Description = description,
Categories = new List<SuggestionHelper>(),
IncludesLikedSongs = IncludesLikedSongs,
IncludesUnCategorizedSongs = IncludesUnCategorizedSongs,
SpotifyPlaylistId = playlist.Id,
CreatedBy = trackedUserEntity,
ExplicitlyExcludedSongs = new List<Song>(),
ExplicitlyIncludedSongs = new List<Song>()
};
var trackedEntity = dci.SmartPlaylistDefinitions.Add(newPlaylist);
await dci.SaveChangesAsync();
return trackedEntity.Entity;
}
}
catch (APIException ex)
{
throw new Exception($"Error creating playlist with title: {playlistTitle}: {ex.Message}", ex);
}
}
public async Task<List<string>> GetSongsInPlaylist(string playlistId)
{
try
{
// for now hardcoded with my user ID
var ids = new List<string>();
var firstContentPage = await UserAuthorizedSpotifyClient.Playlists.GetItems(playlistId);
var allPages = await UserAuthorizedSpotifyClient.PaginateAll(firstContentPage);
ids.AddRange(allPages.Select(track => (track.Track as FullTrack).Id));
return ids;
}
catch (APIException ex)
{
throw new Exception($"Error fetching playlist contents for playlist with id: {playlistId}: {ex.Message}", ex);
}
}
public async Task<string> AddSongsToPlaylist(string playlistId, List<string> songIds)
{
if (songIds.Count == 0)
{
_logger.LogWarning($"No songs to add to playlist with id '{playlistId}'");
return string.Empty;
}
try
{
// for now hardcoded with my user ID
var addItemRequest = new PlaylistAddItemsRequest(songIds.Select(id => $"spotify:track:{id}").ToList());
_logger.LogWarning($"Adding songs to playlist with id '{playlistId}'");
var response = await UserAuthorizedSpotifyClient.Playlists.AddItems(playlistId, addItemRequest);
return response.SnapshotId;
}
catch (APIException ex)
{
throw new Exception($"Error adding songs to playlist with id: {playlistId}: {ex.Message}", ex);
}
}
public async Task<string> RemoveSongsFromPlaylist(string playlistId, List<string> songIds)
{
if (songIds.Count == 0)
{
_logger.LogWarning($"No songs to remove from playlist with id '{playlistId}'");
return string.Empty;
}
try
{
// for now hardcoded with my user ID
var removeItemsRequest = new PlaylistRemoveItemsRequest();
removeItemsRequest.Tracks = new List<PlaylistRemoveItemsRequest.Item>();
foreach (var song in songIds)
{
var item = new PlaylistRemoveItemsRequest.Item()
{
Uri = $"spotify:track:{song}",
};
removeItemsRequest.Tracks.Add(item);
}
_logger.LogWarning($"Removing songs from playlist with id '{playlistId}'");
var response = await UserAuthorizedSpotifyClient.Playlists.RemoveItems(playlistId, removeItemsRequest);
return response.SnapshotId;
}
catch (APIException ex)
{
throw new Exception($"Error removing songs from playlist with id: {playlistId}: {ex.Message}", ex);
}
}
}

View File

@@ -1 +1 @@
0.3.1
0.6.7

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": "*"

View File

@@ -6,16 +6,19 @@
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="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>

View File

@@ -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": {