Compare commits
	
		
			97 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 187d8e92f0 | |||
| 07c7af4633 | |||
| e38023d790 | |||
| 939ae74e95 | |||
| 95406be062 | |||
| 918ed2e667 | |||
| e0b0d6b98c | |||
| 74a8c7dbe8 | |||
| da2a32ecfc | |||
| ef8c8fb867 | |||
| 567f192c46 | |||
| ff5c4588c9 | |||
| 979b7e9fed | |||
| 7457e77867 | |||
| 1f4b5ad34e | |||
| 10cfec30f9 | |||
| d41c4d2b2d | |||
| bc7b16ecb4 | |||
| 081523b510 | |||
| 856e30aacc | |||
| 82946ac812 | |||
| 5b72e25636 | |||
| 2e876ad628 | |||
| 8b91a13095 | |||
| 27a5ca6b74 | |||
| 
						 | 
					8a36606dee | ||
| 
						 | 
					dfc02f6907 | ||
| 
						 | 
					887a8a7b6a | ||
| 
						 | 
					4b18003aa8 | ||
| c0bee8fd3c | |||
| 23afbc1009 | |||
| 220f4d7ffd | |||
| 0d2ec3712e | |||
| 
						 | 
					dbd83ebb6a | ||
| 
						 | 
					010316aa70 | ||
| 
						 | 
					d416242712 | ||
| e9a824c6ef | |||
| e0f206bc0c | |||
| 183309e1ed | |||
| a6321324f7 | |||
| e400249284 | |||
| 73c5e40e1d | |||
| f728c88853 | |||
| 5fdd6ec1d0 | |||
| fbb6d1a409 | |||
| 
						 | 
					d9da54653e | ||
| 
						 | 
					859e96a706 | ||
| 0135b89f01 | |||
| 8c1bbc9866 | |||
| 8100998732 | |||
| 4a77a0d33a | |||
| 3acd4ad9d9 | |||
| 33aae65647 | |||
| 083038d76c | |||
| 055cf79354 | |||
| b4a893d936 | |||
| 31af2e3238 | |||
| 4f4cda622f | |||
| 49d8c2cb08 | |||
| efbbc915e5 | |||
| 
						 | 
					6b9c383697 | ||
| 
						 | 
					92828a90e2 | ||
| 
						 | 
					9fe4613be7 | ||
| 
						 | 
					a09e27281e | ||
| 
						 | 
					8417003ea7 | ||
| 
						 | 
					8412cc13df | ||
| 
						 | 
					7dabcb71f8 | ||
| 
						 | 
					001711487c | ||
| 
						 | 
					bf4bcebfb8 | ||
| 
						 | 
					0561eb757c | ||
| 
						 | 
					5510832538 | ||
| 
						 | 
					604e8dfe70 | ||
| 
						 | 
					2c2a58975e | ||
| 
						 | 
					7572a66ae2 | ||
| 
						 | 
					3ed6c4c54f | ||
| 
						 | 
					f2472a3ea2 | ||
| 
						 | 
					4d657cf887 | ||
| 
						 | 
					1388ce80db | ||
| 
						 | 
					d335ddfd0d | ||
| 
						 | 
					68caf0a329 | ||
| 
						 | 
					2889661f24 | ||
| 
						 | 
					513589a6fc | ||
| 
						 | 
					28616252aa | ||
| 
						 | 
					09acbbefea | ||
| 
						 | 
					9598965a4a | ||
| 
						 | 
					2111eb21e3 | ||
| 
						 | 
					49ec368b2f | ||
| 
						 | 
					a473eccfda | ||
| 
						 | 
					ce82717d62 | ||
| 
						 | 
					3bb39ab17c | ||
| 
						 | 
					a66615c6fc | ||
| 
						 | 
					b054389b16 | ||
| 
						 | 
					71fb945de2 | ||
| b4fab0deee | |||
| 67472ac305 | |||
| e797a7f995 | |||
| 13ac4debdb | 
@@ -46,7 +46,7 @@ create_file() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
get_commit_range() {
 | 
			
		||||
    rm $TEMP_FILE_PATH/messages.txt
 | 
			
		||||
    rm -f $TEMP_FILE_PATH/messages.txt
 | 
			
		||||
    if [[ $LAST_TAG =~ $PATTERN ]]; then
 | 
			
		||||
        create_file true
 | 
			
		||||
    else
 | 
			
		||||
@@ -64,16 +64,16 @@ start() {
 | 
			
		||||
 | 
			
		||||
    while read message; do
 | 
			
		||||
        echo $message
 | 
			
		||||
        if echo $message | grep -Pq '(feat|style)(\([\w]+\))?!:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(NOISSUE)))'; then
 | 
			
		||||
        if echo $message | grep -Pq '(feat|style)(\([\w]+\))?!:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(#[0-9]+)|(NOISSUE)))'; then
 | 
			
		||||
            increment_type="major"
 | 
			
		||||
            echo "a"
 | 
			
		||||
            break
 | 
			
		||||
        elif echo $message | grep -Pq '(feat|style)(\([\w]+\))?:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(NOISSUE)))'; then
 | 
			
		||||
        elif echo $message | grep -Pq '(feat|style)(\([\w]+\))?:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(#[0-9]+)|(NOISSUE)))'; then
 | 
			
		||||
            if [ -z "$increment_type" ] || [ "$increment_type" == "patch" ]; then
 | 
			
		||||
                increment_type="minor"
 | 
			
		||||
                echo "b"
 | 
			
		||||
            fi 
 | 
			
		||||
        elif echo $message | grep -Pq '(build|fix|perf|refactor|revert)(\(.+\))?:\s([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(NOISSUE)))'; then
 | 
			
		||||
        elif echo $message | grep -Pq '(build|fix|perf|refactor|revert)(\(.+\))?:\s([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(#[0-9]+)|(NOISSUE)))'; then
 | 
			
		||||
            if [ -z "$increment_type" ]; then
 | 
			
		||||
                increment_type="patch"
 | 
			
		||||
                echo "c"
 | 
			
		||||
@@ -86,8 +86,9 @@ start() {
 | 
			
		||||
        echo "New version: $new_version"
 | 
			
		||||
 | 
			
		||||
        gitchangelog | grep -v "[rR]elease:" > HISTORY.md
 | 
			
		||||
        git add song_of_the_day/VERSION HISTORY.md
 | 
			
		||||
        git add HISTORY.md
 | 
			
		||||
        echo $new_version > song_of_the_day/VERSION
 | 
			
		||||
        git add song_of_the_day/VERSION 
 | 
			
		||||
        git commit -m "release: version $new_version 🚀"
 | 
			
		||||
        echo "creating git tag : $new_version"
 | 
			
		||||
        git tag $new_version
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
name: Upload Python Package
 | 
			
		||||
name: Build Docker image
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: write
 | 
			
		||||
 | 
			
		||||
@@ -38,10 +38,6 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v3
 | 
			
		||||
    - name: Set up dotnet
 | 
			
		||||
      uses: actions/setup-dotnet@v4
 | 
			
		||||
      with:
 | 
			
		||||
        dotnet-version: '9.0.X'
 | 
			
		||||
    - name: Check version match
 | 
			
		||||
      run: |
 | 
			
		||||
        REPOSITORY_NAME=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $2}' | tr '-' '_')
 | 
			
		||||
@@ -59,7 +55,7 @@ jobs:
 | 
			
		||||
        registry: git.disi.dev
 | 
			
		||||
    - name: Build and publish
 | 
			
		||||
      run: |
 | 
			
		||||
        REPOSITORY_OWNER=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $1}')
 | 
			
		||||
        REPOSITORY_OWNER=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $1}' | tr '[:upper:]' '[:lower:]')
 | 
			
		||||
        REPOSITORY_NAME=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $2}' | tr '-' '_')
 | 
			
		||||
        docker build -t "git.disi.dev/$REPOSITORY_OWNER/song-of-the-day:$(cat $REPOSITORY_NAME/VERSION)" ./song_of_the_day
 | 
			
		||||
        docker build -t "git.disi.dev/$REPOSITORY_OWNER/song-of-the-day:$(cat $REPOSITORY_NAME/VERSION)" ./
 | 
			
		||||
        docker push "git.disi.dev/$REPOSITORY_OWNER/song-of-the-day:$(cat $REPOSITORY_NAME/VERSION)"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								CronTimer/CronEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								CronTimer/CronEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
public class CronTimerEventArgs : EventArgs
 | 
			
		||||
{
 | 
			
		||||
    public DateTime At { get; set; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								CronTimer/CronTimer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								CronTimer/CronTimer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using NCrontab;
 | 
			
		||||
 | 
			
		||||
public class CronTimer
 | 
			
		||||
{
 | 
			
		||||
    public const string UTC = "Etc/UTC";
 | 
			
		||||
 | 
			
		||||
    static readonly TimeSpan InfiniteTimeSpan = TimeSpan.FromMilliseconds(Timeout.Infinite); // net 3.5
 | 
			
		||||
 | 
			
		||||
    readonly CrontabSchedule schedule;
 | 
			
		||||
    readonly TimeZoneInfo tzi;
 | 
			
		||||
    readonly string id;
 | 
			
		||||
    readonly Timer t;
 | 
			
		||||
 | 
			
		||||
    public string tz { get; }
 | 
			
		||||
    public string Expression { get; }
 | 
			
		||||
    public event EventHandler<CronTimerEventArgs> OnOccurence;
 | 
			
		||||
 | 
			
		||||
    public DateTime Next { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public CronTimer(string expression, string tz = UTC, bool includingSeconds = false)
 | 
			
		||||
    {
 | 
			
		||||
        Expression = expression;
 | 
			
		||||
        this.tz = tz;
 | 
			
		||||
        id = TimeZoneConverter.TZConvert.IanaToWindows(tz);
 | 
			
		||||
        tzi = TimeZoneInfo.FindSystemTimeZoneById(id);
 | 
			
		||||
        schedule = CrontabSchedule.Parse(expression, new CrontabSchedule.ParseOptions { IncludingSeconds = includingSeconds });
 | 
			
		||||
        Next = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
 | 
			
		||||
        OnOccurence += OnOccurenceScheduleNext;
 | 
			
		||||
        t = new Timer(s =>
 | 
			
		||||
        {
 | 
			
		||||
            var ea = new CronTimerEventArgs
 | 
			
		||||
            {
 | 
			
		||||
                At = Next
 | 
			
		||||
            };
 | 
			
		||||
            OnOccurence(this, ea);
 | 
			
		||||
        }, null, InfiniteTimeSpan, InfiniteTimeSpan);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void OnOccurenceScheduleNext(object sender, EventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var delay = CalculateDelay();
 | 
			
		||||
        //Console.WriteLine($"Next for [{tz} {expression}] in {delay}.");
 | 
			
		||||
        t.Change(delay, InfiniteTimeSpan);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Start()
 | 
			
		||||
    {
 | 
			
		||||
        var delay = CalculateDelay();
 | 
			
		||||
        //Console.WriteLine($"Next for [{tz} {expression}] in {delay}.");
 | 
			
		||||
        t.Change(delay, InfiniteTimeSpan);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    TimeSpan CalculateDelay()
 | 
			
		||||
    {
 | 
			
		||||
        var nowUtc = DateTime.UtcNow;
 | 
			
		||||
        Next = schedule.GetNextOccurrence(Next);
 | 
			
		||||
        TimeSpan delay;
 | 
			
		||||
        if (tz != UTC)
 | 
			
		||||
        {
 | 
			
		||||
            var nextUtc = TimeZoneInfo.ConvertTimeToUtc(Next, tzi);
 | 
			
		||||
            delay = nextUtc - nowUtc;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            delay = Next - nowUtc;
 | 
			
		||||
        }
 | 
			
		||||
        //Console.WriteLine($"Now: {nowUtc} [utc] {now} [{tz}], Next: {next} [{tz}] {nextUtc} [utc], Delay: {delay}");
 | 
			
		||||
        if (delay < TimeSpan.Zero) delay = TimeSpan.Zero;
 | 
			
		||||
        return delay;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Stop()
 | 
			
		||||
    {
 | 
			
		||||
        t.Change(InfiniteTimeSpan, InfiniteTimeSpan);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								CronTimer/CronTimer.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								CronTimer/CronTimer.csproj
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <LangVersion>latest</LangVersion>
 | 
			
		||||
    <TargetFrameworks>net461;netstandard2.1</TargetFrameworks>
 | 
			
		||||
    <PlatformTarget>AnyCPU</PlatformTarget>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <!-- AssemblyFileVersionAttribute -->
 | 
			
		||||
    <FileVersion>2.0.1</FileVersion>
 | 
			
		||||
    <!-- AssemblyInformationalVersionAttribute -->
 | 
			
		||||
    <Version>$(FileVersion)</Version>
 | 
			
		||||
    <!-- AssemblyVersionAttribute -->
 | 
			
		||||
    <AssemblyVersion>2.0.0.0</AssemblyVersion>
 | 
			
		||||
    <!-- Nuget -->
 | 
			
		||||
    <PackageVersion>$(Version)</PackageVersion>
 | 
			
		||||
    <PackageId>CronTimer</PackageId>
 | 
			
		||||
    <Company>https://github.com/ramonsmits</Company>
 | 
			
		||||
    <Authors>ramonsmits</Authors>
 | 
			
		||||
    <Description>Simple .net Timer that is based on cron expressions with second accuracy to fire timer events to a very specific schedule.</Description>
 | 
			
		||||
    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
 | 
			
		||||
    <PackageReleaseNotes></PackageReleaseNotes>
 | 
			
		||||
    <PackageProjectUrl>https://github.com/ramonsmits/CronTimer/tree/$(PackageVersion)</PackageProjectUrl>
 | 
			
		||||
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
 | 
			
		||||
    <IncludeSymbols>True</IncludeSymbols>
 | 
			
		||||
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
 | 
			
		||||
    <IncludeSource>True</IncludeSource>
 | 
			
		||||
    <RepositoryUrl>https://github.com/ramonsmits/CronTimer</RepositoryUrl>
 | 
			
		||||
    <Copyright>Copyright 2022 (c) Ramon Smits</Copyright>
 | 
			
		||||
    <PackageTags>cron timer</PackageTags>
 | 
			
		||||
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
 | 
			
		||||
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 | 
			
		||||
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
 | 
			
		||||
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="ncrontab" Version="3.3.1" />
 | 
			
		||||
    <PackageReference Include="TimeZoneConverter" Version="6.0.1" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <!--<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
 | 
			
		||||
      <Version>3.1.4</Version>
 | 
			
		||||
    </PackageReference>
 | 
			
		||||
  </ItemGroup>-->
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										36
									
								
								CronTimer/CronTimer.sln
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								CronTimer/CronTimer.sln
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
 | 
			
		||||
Microsoft Visual Studio Solution File, Format Version 12.00
 | 
			
		||||
# Visual Studio Version 16
 | 
			
		||||
VisualStudioVersion = 16.0.30717.126
 | 
			
		||||
MinimumVisualStudioVersion = 10.0.40219.1
 | 
			
		||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CronTimer", "CronTimer.csproj", "{FB64C227-8615-4AE1-94E3-F9F9DF192B72}"
 | 
			
		||||
EndProject
 | 
			
		||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CCDD0B34-653C-430C-9B17-5129618F8D7D}"
 | 
			
		||||
	ProjectSection(SolutionItems) = preProject
 | 
			
		||||
		..\README.md = ..\README.md
 | 
			
		||||
	EndProjectSection
 | 
			
		||||
EndProject
 | 
			
		||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "..\Demo\Demo.csproj", "{C2638357-1621-4422-8701-B55BFB37ACCF}"
 | 
			
		||||
EndProject
 | 
			
		||||
Global
 | 
			
		||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
			
		||||
		Debug|Any CPU = Debug|Any CPU
 | 
			
		||||
		Release|Any CPU = Release|Any CPU
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 | 
			
		||||
		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
		{C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(SolutionProperties) = preSolution
 | 
			
		||||
		HideSolutionNode = FALSE
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ExtensibilityGlobals) = postSolution
 | 
			
		||||
		SolutionGuid = {A639B164-6581-40DF-ADC4-479DAB467CFE}
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
EndGlobal
 | 
			
		||||
@@ -3,13 +3,17 @@ WORKDIR /App
 | 
			
		||||
 | 
			
		||||
# Copy everything
 | 
			
		||||
COPY . ./
 | 
			
		||||
 | 
			
		||||
RUN apt update && apt install libldap-2.5-0 -y
 | 
			
		||||
 | 
			
		||||
# Restore as distinct layers
 | 
			
		||||
RUN dotnet restore
 | 
			
		||||
RUN dotnet restore ./song_of_the_day/song_of_the_day.csproj
 | 
			
		||||
# Build and publish a release
 | 
			
		||||
RUN dotnet publish -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
 | 
			
		||||
WORKDIR /App
 | 
			
		||||
RUN apt update && apt install libldap-2.5-0 -y
 | 
			
		||||
COPY --from=build /App/out .
 | 
			
		||||
ENTRYPOINT ["dotnet", "song_of_the_day.dll"]
 | 
			
		||||
							
								
								
									
										490
									
								
								HISTORY.md
									
									
									
									
									
								
							
							
						
						
									
										490
									
								
								HISTORY.md
									
									
									
									
									
								
							@@ -4,6 +4,496 @@ Changelog
 | 
			
		||||
 | 
			
		||||
(unreleased)
 | 
			
		||||
------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Proper Spotify token refresh flow, refs: NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.6.9 (2025-07-20)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Improved Spotify auth check flow, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.6.8 (2025-07-20)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Improved Spotify auth check flow, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.6.7 (2025-07-20)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Attempted bugfix for crashing process on invalid spotify access token,
 | 
			
		||||
  refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
------------------
 | 
			
		||||
- Ci: more CI fixes, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
- Ci: improve commit message generation script, refs NOISSUE. [Simon
 | 
			
		||||
  Diesenreiter]
 | 
			
		||||
- Feat: keep track of user submissions, refs #4. [Simon Diesenreiter]
 | 
			
		||||
- Feat: save submission history, refs #7. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.6 (2025-05-22)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Bugfixes, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
- Reduce number of emitted logs. [simon]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.5 (2025-05-18)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Install dependencies in runtime container not only build container,
 | 
			
		||||
  refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.4 (2025-05-18)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Build errors, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.3 (2025-05-18)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Resolve linting errors, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.2 (2025-05-18)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Also send pick suggestion to the group, refs NOISSUE. [Simon
 | 
			
		||||
  Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.1 (2025-05-17)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Fix Docker build, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.2.0 (2025-05-17)
 | 
			
		||||
------------------
 | 
			
		||||
- Feat: add user management, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
- Feat(auth): initial auth added part 2, refs NOISSUE. [Simon
 | 
			
		||||
  Diesenreiter]
 | 
			
		||||
- Feat(auth): initial auth added, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.21 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Fix new user saving refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.20 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Remove unnecessary dotnet runtime download in CI job refs NOISSUE.
 | 
			
		||||
  [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.19 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Improve Docker build refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.18 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Remove broken Crontimer NuGet feed reference refs NOISSUE. [Simon
 | 
			
		||||
  Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.17 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Remove uinnecessary NuGet login refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.16 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Add direct CronTimer reference refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.15 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Login to NuGet feed at build refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.14 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Fix typo refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.13 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Really fix Crontab dependency refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.12 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Hotfixed CronTab dependencyrefs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.11 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- More debug outputs refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.10 (2025-04-15)
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Release version jumbled up refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.9 (2025-04-15)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Additional debug outputs refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.7 (2025-04-15)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- More fixes in release logic refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.6 (2025-04-15)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Makefile issues on sh refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.5 (2025-04-14)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Messed up release refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.4 (2025-04-14)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Repo casing refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.3 (2025-04-14)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.2 (2025-04-14)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.1 (2025-04-14)
 | 
			
		||||
------------------
 | 
			
		||||
 | 
			
		||||
Fix
 | 
			
		||||
~~~
 | 
			
		||||
- Correct container repo path, refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
 | 
			
		||||
Other
 | 
			
		||||
~~~~~
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
0.1.0 (2025-04-14)
 | 
			
		||||
------------------
 | 
			
		||||
- Feat: add release workflow refs NOISSUE. [Simon Diesenreiter]
 | 
			
		||||
- Feat: initial working version of service refs NOISSUE. [Simon
 | 
			
		||||
  Diesenreiter]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
.PHONY: issetup
 | 
			
		||||
issetup:
 | 
			
		||||
	@[ -f .git/hooks/commit-msg ] || [ -v SKIP_MAKE_SETUP_CHECK ] || (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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								nuget.config
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								nuget.config
									
									
									
									
									
								
							@@ -1,11 +1,11 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<configuration>
 | 
			
		||||
  <packageSources>
 | 
			
		||||
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
 | 
			
		||||
    <clear />
 | 
			
		||||
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
 | 
			
		||||
    <!--add key="gitea-projects" value="https://git.disi.dev/api/packages/Projects/nuget/index.json" />
 | 
			
		||||
    <add key="gitea-homelab" value="https://git.disi.dev/api/packages/Homelab/nuget/index.json" />
 | 
			
		||||
    <add key="gitea-artifacts" value="https://git.disi.dev/api/packages/Artifacts/nuget/index.json" /-->
 | 
			
		||||
    <!--add key="gitea-projects" value="https://git.disi.dev/api/packages/Projects/nuget/index.json" /-->
 | 
			
		||||
    <!--add key="gitea-artifacts" value="https://git.disi.dev/api/packages/Artifacts/nuget/index.json" /-->
 | 
			
		||||
    <add key="HomeLab" value="https://git.disi.dev/api/packages/HomeLab/nuget/index.json" />
 | 
			
		||||
  </packageSources>
 | 
			
		||||
</configuration>
 | 
			
		||||
</configuration>
 | 
			
		||||
							
								
								
									
										6
									
								
								song_of_the_day/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								song_of_the_day/.editorconfig
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										46
									
								
								song_of_the_day/.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										47
									
								
								song_of_the_day/.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal 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"
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								song_of_the_day/Auth/ConfigurationAD.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								song_of_the_day/Auth/ConfigurationAD.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
public class ConfigurationAD
 | 
			
		||||
{
 | 
			
		||||
    public int Port { get; set; } = 389;
 | 
			
		||||
    public string Zone { get; set; } = string.Empty;
 | 
			
		||||
    public string Domain { get; set; } = string.Empty;
 | 
			
		||||
    public string Subdomain { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    public string Username { get; set; } = string.Empty;
 | 
			
		||||
    public string Password { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    public string LDAPserver { get; set; } = string.Empty;
 | 
			
		||||
    public string LDAPQueryBase { get; set; } = string.Empty;
 | 
			
		||||
    public string LDAPUserQueryBase { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    public string CrewGroup { get; set; } = string.Empty;
 | 
			
		||||
    public string ManagerGroup { get; set; } = string.Empty;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								song_of_the_day/Auth/IAuthenticationService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								song_of_the_day/Auth/IAuthenticationService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
public interface IAuthenticationService
 | 
			
		||||
{
 | 
			
		||||
    bool Authenticate(string userName, string password);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								song_of_the_day/Auth/LdapAuthenticationService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								song_of_the_day/Auth/LdapAuthenticationService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
public class LdapAuthenticationService : IAuthenticationService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IConfiguration _configuration;
 | 
			
		||||
    private LdapIntegration _ldapIntegration;
 | 
			
		||||
 | 
			
		||||
    public LdapAuthenticationService(IConfiguration configuration, LdapIntegration ldapIntegration)
 | 
			
		||||
    {
 | 
			
		||||
        _configuration = configuration;
 | 
			
		||||
        _ldapIntegration = ldapIntegration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool Authenticate(string username, string password)
 | 
			
		||||
    {
 | 
			
		||||
        return _ldapIntegration == null ? false : _ldapIntegration.TestLogin(username, password);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								song_of_the_day/Auth/PhoneClaimCodeProviderService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								song_of_the_day/Auth/PhoneClaimCodeProviderService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
public class PhoneClaimCodeProviderService
 | 
			
		||||
{
 | 
			
		||||
    private Dictionary<string, string> _phoneClaimCodes;
 | 
			
		||||
    private Dictionary<string, string> _phoneClaimNumbers;
 | 
			
		||||
    private SignalIntegration _signalIntegration;
 | 
			
		||||
 | 
			
		||||
    public PhoneClaimCodeProviderService(SignalIntegration signalIntegration)
 | 
			
		||||
    {
 | 
			
		||||
        _phoneClaimCodes = new Dictionary<string, string>();
 | 
			
		||||
        _phoneClaimNumbers = new Dictionary<string, string>();
 | 
			
		||||
        _signalIntegration = signalIntegration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 async void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
 | 
			
		||||
    {
 | 
			
		||||
        var generatedCode = string.Empty;
 | 
			
		||||
        if (IsCodeGeneratedForUser(username))
 | 
			
		||||
        {
 | 
			
		||||
            generatedCode = _phoneClaimCodes[username];
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            generatedCode = RandomString(6);
 | 
			
		||||
            _phoneClaimCodes[username] = generatedCode;
 | 
			
		||||
            _phoneClaimNumbers[username] = phoneNumber;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (_signalIntegration != null)
 | 
			
		||||
        {
 | 
			
		||||
            await _signalIntegration.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string ValidateClaimCodeForUser(string code, string username)
 | 
			
		||||
    {
 | 
			
		||||
        var result = false;
 | 
			
		||||
        result = _phoneClaimCodes[username] == code;
 | 
			
		||||
 | 
			
		||||
        if (result)
 | 
			
		||||
        {
 | 
			
		||||
            _phoneClaimCodes.Remove(username);
 | 
			
		||||
            var number = _phoneClaimNumbers[username];
 | 
			
		||||
            _phoneClaimNumbers.Remove(username);
 | 
			
		||||
            return number;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool IsCodeGeneratedForUser(string username)
 | 
			
		||||
    {
 | 
			
		||||
        return _phoneClaimCodes.ContainsKey(username);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,6 +16,30 @@ public class AppConfiguration
 | 
			
		||||
        this.SignalGroupId = Environment.GetEnvironmentVariable("SIGNAL_GROUP_ID") ?? "group.Wmk1UTVQTnh0Sjd6a0xiOGhnTnMzZlNkc2p2Q3c0SXJiQkU2eDlNU0hyTT0=";
 | 
			
		||||
        this.WebUIBaseURL = Environment.GetEnvironmentVariable("WEB_BASE_URL") ?? "https://sotd.disi.dev/";
 | 
			
		||||
        this.UseBotTag = bool.Parse(Environment.GetEnvironmentVariable("USE_BOT_TAG") ?? "true");
 | 
			
		||||
        this.DaysBetweenRequests = int.Parse(Environment.GetEnvironmentVariable("DAYS_BETWEEN_REQUESTS") ?? "2");
 | 
			
		||||
        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",
 | 
			
		||||
            Password = Environment.GetEnvironmentVariable("LDAP_PASS") ?? "adminPass2022!",
 | 
			
		||||
            Port = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LDAP_BIND")) ? int.Parse(bindValue ?? "389") : 389,
 | 
			
		||||
            LDAPserver = Environment.GetEnvironmentVariable("LDAP_URL") ?? "192.168.1.108",
 | 
			
		||||
            LDAPQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "dc=disi,dc=dev",
 | 
			
		||||
            LDAPUserQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "ou=people,dc=disi,dc=dev",
 | 
			
		||||
            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
 | 
			
		||||
@@ -68,8 +92,58 @@ public class AppConfiguration
 | 
			
		||||
        get; private set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string SpotifyClientId
 | 
			
		||||
    {
 | 
			
		||||
        get; private set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string SpotifyClientSecret
 | 
			
		||||
    {
 | 
			
		||||
        get; private set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool UseBotTag
 | 
			
		||||
    {
 | 
			
		||||
        get; private set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int DaysBetweenRequests
 | 
			
		||||
    {
 | 
			
		||||
        get; private set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ConfigurationAD LDAPConfig
 | 
			
		||||
    {
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								song_of_the_day/Controllers/AuthController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								song_of_the_day/Controllers/AuthController.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using Microsoft.AspNetCore.Authentication.Cookies;
 | 
			
		||||
using Microsoft.AspNetCore.Authentication;
 | 
			
		||||
 | 
			
		||||
public class AuthController : Controller
 | 
			
		||||
{
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    public async Task<IActionResult> Login(string username, string password)
 | 
			
		||||
    {
 | 
			
		||||
        var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
 | 
			
		||||
        if (ldapService != null && ldapService.Authenticate(username, password))
 | 
			
		||||
        {
 | 
			
		||||
            var claims = new[] { new Claim(ClaimTypes.Name, username) };
 | 
			
		||||
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
 | 
			
		||||
            await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
 | 
			
		||||
            return RedirectToSamePageIfPossible();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ViewBag.Error = "Invalid credentials";
 | 
			
		||||
        return RedirectToSamePageIfPossible();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    public async Task<IActionResult> Logout()
 | 
			
		||||
    {
 | 
			
		||||
        await HttpContext.SignOutAsync();
 | 
			
		||||
        return RedirectToSamePageIfPossible();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IActionResult RedirectToSamePageIfPossible()
 | 
			
		||||
    {
 | 
			
		||||
        if (Request.Headers.ContainsKey("Referer"))
 | 
			
		||||
        {
 | 
			
		||||
            return Redirect(Request.Headers["Referer"].ToString());
 | 
			
		||||
        }
 | 
			
		||||
        return RedirectToPage("/");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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"));   
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								song_of_the_day/Data/Migrations/20250516161831_UpdateUserModel.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								song_of_the_day/Data/Migrations/20250516161831_UpdateUserModel.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
// <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("20250516161831_UpdateUserModel")]
 | 
			
		||||
    partial class UpdateUserModel
 | 
			
		||||
    {
 | 
			
		||||
        /// <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<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<int?>("SongId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("UserId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SongId");
 | 
			
		||||
 | 
			
		||||
                    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.HasKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Users");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("Song", "Song")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("User", "User")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Song");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("User");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,265 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class UpdateUserModel : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Songs_SongId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Users_UserId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "SignalMemberId",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "NickName",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Name",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<bool>(
 | 
			
		||||
                name: "AssociationInProgress",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: false);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<string>(
 | 
			
		||||
                name: "LdapUserName",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Title",
 | 
			
		||||
                table: "SuggestionHelpers",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Description",
 | 
			
		||||
                table: "SuggestionHelpers",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "UserId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "SongId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Url",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Name",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Artist",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Songs_SongId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                column: "SongId",
 | 
			
		||||
                principalTable: "Songs",
 | 
			
		||||
                principalColumn: "SongId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Users_UserId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                column: "UserId",
 | 
			
		||||
                principalTable: "Users",
 | 
			
		||||
                principalColumn: "UserId");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Songs_SongId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Users_UserId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "AssociationInProgress",
 | 
			
		||||
                table: "Users");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "LdapUserName",
 | 
			
		||||
                table: "Users");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "SignalMemberId",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "NickName",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Name",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Title",
 | 
			
		||||
                table: "SuggestionHelpers",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Description",
 | 
			
		||||
                table: "SuggestionHelpers",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "UserId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "SongId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Url",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Name",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "Artist",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "",
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "text",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Songs_SongId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                column: "SongId",
 | 
			
		||||
                principalTable: "Songs",
 | 
			
		||||
                principalColumn: "SongId",
 | 
			
		||||
                onDelete: ReferentialAction.Cascade);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_Users_UserId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                column: "UserId",
 | 
			
		||||
                principalTable: "Users",
 | 
			
		||||
                principalColumn: "UserId",
 | 
			
		||||
                onDelete: ReferentialAction.Cascade);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										145
									
								
								song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
// <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("20250524160218_additional data for song submissions")]
 | 
			
		||||
    partial class additionaldataforsongsubmissions
 | 
			
		||||
    {
 | 
			
		||||
        /// <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<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<int?>("SongId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("Submitted")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("UserId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SongId");
 | 
			
		||||
 | 
			
		||||
                    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.HasKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Users");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("Song", "Song")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("User", "User")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Song");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("User");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class additionaldataforsongsubmissions : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AddColumn<bool>(
 | 
			
		||||
                name: "Submitted",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "Submitted",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										164
									
								
								song_of_the_day/Data/Migrations/20250524164159_keep track of users oicked for submission.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								song_of_the_day/Data/Migrations/20250524164159_keep track of users oicked for submission.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
// <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("20250524164159_keep track of users oicked for submission")]
 | 
			
		||||
    partial class keeptrackofusersoickedforsubmission
 | 
			
		||||
    {
 | 
			
		||||
        /// <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<string>("Url")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Songs");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("Date")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasUsedSuggestion")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("SongId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("SuggestionHelperId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("UserHasSubmitted")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("UserId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SuggestionHelperId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SongSuggestions");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SuggestionHelper", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Title")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SuggestionHelpers");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("User", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("UserId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("AssociationInProgress")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsIntroduced")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("LdapUserName")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("NickName")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SignalMemberId")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("WasChosenForSuggestionThisRound")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Users");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("Song", "Song")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("SuggestionHelper", "SuggestionHelper")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SuggestionHelperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired();
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("User", "User")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Song");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("SuggestionHelper");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("User");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,82 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class keeptrackofusersoickedforsubmission : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "Submitted",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                newName: "UserHasSubmitted");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<bool>(
 | 
			
		||||
                name: "WasChosenForSuggestionThisRound",
 | 
			
		||||
                table: "Users",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: false);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<bool>(
 | 
			
		||||
                name: "HasUsedSuggestion",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: false);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<int>(
 | 
			
		||||
                name: "SuggestionHelperId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_SongSuggestions_SuggestionHelperId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                column: "SuggestionHelperId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_SuggestionHelpers_SuggestionHelperId",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                column: "SuggestionHelperId",
 | 
			
		||||
                principalTable: "SuggestionHelpers",
 | 
			
		||||
                principalColumn: "Id",
 | 
			
		||||
                onDelete: ReferentialAction.Cascade);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "FK_SongSuggestions_SuggestionHelpers_SuggestionHelperId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropIndex(
 | 
			
		||||
                name: "IX_SongSuggestions_SuggestionHelperId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "WasChosenForSuggestionThisRound",
 | 
			
		||||
                table: "Users");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "HasUsedSuggestion",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "SuggestionHelperId",
 | 
			
		||||
                table: "SongSuggestions");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "UserHasSubmitted",
 | 
			
		||||
                table: "SongSuggestions",
 | 
			
		||||
                newName: "Submitted");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								song_of_the_day/Data/Migrations/20250601144913_some more model updates.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								song_of_the_day/Data/Migrations/20250601144913_some more model updates.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(DataContext))]
 | 
			
		||||
    [Migration("20250601144913_some more model updates")]
 | 
			
		||||
    partial class somemoremodelupdates
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.3")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("Song", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("SongId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Artist")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("Provider")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SpotifyId")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Url")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Songs");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("Date")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasUsedSuggestion")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("SongId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("SuggestionHelperId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("UserHasSubmitted")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("UserId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SuggestionHelperId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SongSuggestions");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SuggestionHelper", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Title")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SuggestionHelpers");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("User", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("UserId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("AssociationInProgress")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsIntroduced")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("LdapUserName")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("NickName")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SignalMemberId")
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("WasChosenForSuggestionThisRound")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Users");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("SongSuggestion", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("Song", "Song")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("SuggestionHelper", "SuggestionHelper")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SuggestionHelperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired();
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("User", "User")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Song");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("SuggestionHelper");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("User");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace song_of_the_day.DataMigrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class somemoremodelupdates : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AddColumn<int>(
 | 
			
		||||
                name: "Provider",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<string>(
 | 
			
		||||
                name: "SpotifyId",
 | 
			
		||||
                table: "Songs",
 | 
			
		||||
                type: "text",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "Provider",
 | 
			
		||||
                table: "Songs");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "SpotifyId",
 | 
			
		||||
                table: "Songs");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										268
									
								
								song_of_the_day/Data/Migrations/20250719185147_adding song likes and playlists.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								song_of_the_day/Data/Migrations/20250719185147_adding song likes and playlists.Designer.cs
									
									
									
										generated
									
									
									
										Normal 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										282
									
								
								song_of_the_day/Data/Migrations/20250719230009_some more fixes.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								song_of_the_day/Data/Migrations/20250719230009_some more fixes.Designer.cs
									
									
									
										generated
									
									
									
										Normal 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										282
									
								
								song_of_the_day/Data/Migrations/20250719233912_explicitly specify keys.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								song_of_the_day/Data/Migrations/20250719233912_explicitly specify keys.Designer.cs
									
									
									
										generated
									
									
									
										Normal 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										280
									
								
								song_of_the_day/Data/Migrations/20250720003809_fix auth token non-null contraint.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								song_of_the_day/Data/Migrations/20250720003809_fix auth token non-null contraint.Designer.cs
									
									
									
										generated
									
									
									
										Normal 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										296
									
								
								song_of_the_day/Data/Migrations/20250720144512_explicitly model liked songs relationship.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								song_of_the_day/Data/Migrations/20250720144512_explicitly model liked songs relationship.Designer.cs
									
									
									
										generated
									
									
									
										Normal 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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")
 | 
			
		||||
@@ -30,19 +78,32 @@ namespace song_of_the_day.DataMigrations
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Artist")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .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")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("SongId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SmartPlaylistDefinitionId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SmartPlaylistDefinitionId1");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Songs");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
@@ -57,16 +118,27 @@ namespace song_of_the_day.DataMigrations
 | 
			
		||||
                    b.Property<DateTime>("Date")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("SongId")
 | 
			
		||||
                    b.Property<bool>("HasUsedSuggestion")
 | 
			
		||||
                        .HasColumnType("boolean");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("SongId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("UserId")
 | 
			
		||||
                    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");
 | 
			
		||||
@@ -81,15 +153,18 @@ namespace song_of_the_day.DataMigrations
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("SmartPlaylistDefinitionId")
 | 
			
		||||
                        .HasColumnType("integer");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Title")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("SmartPlaylistDefinitionId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("SuggestionHelpers");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
@@ -101,44 +176,117 @@ namespace song_of_the_day.DataMigrations
 | 
			
		||||
 | 
			
		||||
                    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")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("NickName")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("SignalMemberId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .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("SongSuggestion", b =>
 | 
			
		||||
            modelBuilder.Entity("LikedSongs", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("Song", "Song")
 | 
			
		||||
                    b.HasOne("Song", null)
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("SongId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired();
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("User", "User")
 | 
			
		||||
                    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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								song_of_the_day/Data/SmartPlaylistDefinition.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								song_of_the_day/Data/SmartPlaylistDefinition.cs
									
									
									
									
									
										Normal 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								song_of_the_day/Data/SongProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								song_of_the_day/Data/SongProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
public enum SongProvider
 | 
			
		||||
{
 | 
			
		||||
    Spotify,
 | 
			
		||||
    YouTube,
 | 
			
		||||
    YoutubeMusic,
 | 
			
		||||
    SoundCloud,
 | 
			
		||||
    Bandcamp,
 | 
			
		||||
    PlainHttp,
 | 
			
		||||
    NavidromeSharedLink,
 | 
			
		||||
    Other
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
public class SongSuggestion
 | 
			
		||||
@@ -7,4 +8,7 @@ public class SongSuggestion
 | 
			
		||||
    public User? User { get; set; }
 | 
			
		||||
    public Song? Song { get; set; }
 | 
			
		||||
    public DateTime Date { get; set; }
 | 
			
		||||
    public bool UserHasSubmitted { get; set; }
 | 
			
		||||
    public required SuggestionHelper SuggestionHelper { get; set; }
 | 
			
		||||
    public bool HasUsedSuggestion { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,4 +7,20 @@ public class User
 | 
			
		||||
    public string? Name { get; set; }
 | 
			
		||||
    public string? NickName { get; set; }
 | 
			
		||||
    public bool IsIntroduced { get; set; }
 | 
			
		||||
    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; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								song_of_the_day/GlobalSuppressions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								song_of_the_day/GlobalSuppressions.cs
									
									
									
									
									
										Normal 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")]
 | 
			
		||||
							
								
								
									
										7
									
								
								song_of_the_day/LDAPIntegration/Data/LdapUser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								song_of_the_day/LDAPIntegration/Data/LdapUser.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
public class LdapUser
 | 
			
		||||
{
 | 
			
		||||
    public string? UserId { get; set; }
 | 
			
		||||
    public string? FirstName { get; set; }
 | 
			
		||||
    public string? LastName { get; set; }
 | 
			
		||||
    public string? Email { get; set; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										126
									
								
								song_of_the_day/LDAPIntegration/LdapIntegration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								song_of_the_day/LDAPIntegration/LdapIntegration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
using System.Collections;
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using song_of_the_day;
 | 
			
		||||
using System.DirectoryServices.Protocols;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
 | 
			
		||||
public class LdapIntegration
 | 
			
		||||
{
 | 
			
		||||
    private readonly string[] attributesToQuery = new string[]
 | 
			
		||||
        {
 | 
			
		||||
            "uid",
 | 
			
		||||
            "givenName",
 | 
			
		||||
            "sn",
 | 
			
		||||
            "mail"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public LdapIntegration(ILogger<LdapIntegration> logger)
 | 
			
		||||
    {
 | 
			
		||||
        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; }
 | 
			
		||||
 | 
			
		||||
    private int Port { get; set; }
 | 
			
		||||
 | 
			
		||||
    private string AdminBind { get; set; }
 | 
			
		||||
 | 
			
		||||
    private string AdminPass { get; set; }
 | 
			
		||||
 | 
			
		||||
    private ILogger<LdapIntegration> logger { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool TestLogin(string username, string password)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var userList = this.SearchInADAsUser(
 | 
			
		||||
                AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
 | 
			
		||||
                $"(uid={username})",
 | 
			
		||||
                SearchScope.Subtree,
 | 
			
		||||
                username,
 | 
			
		||||
                password);
 | 
			
		||||
        }
 | 
			
		||||
        catch (LdapException ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (ex.Message.Contains("credential is invalid"))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            throw;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<LdapUser> SearchInAD(
 | 
			
		||||
        string targetOU,
 | 
			
		||||
        string query,
 | 
			
		||||
        SearchScope scope
 | 
			
		||||
        )
 | 
			
		||||
    {
 | 
			
		||||
        // search as admin
 | 
			
		||||
        return this.SearchInADAsUser(targetOU, query, scope, this.AdminBind, this.AdminPass);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<LdapUser> SearchInADAsUser(
 | 
			
		||||
        string targetOU,
 | 
			
		||||
        string query,
 | 
			
		||||
        SearchScope scope,
 | 
			
		||||
        string userName,
 | 
			
		||||
        string userPass
 | 
			
		||||
        )
 | 
			
		||||
    {
 | 
			
		||||
        // on Windows the authentication type is Negotiate, so there is no need to prepend
 | 
			
		||||
        // AD user login with domain. On other platforms at the moment only
 | 
			
		||||
        // Basic authentication is supported
 | 
			
		||||
        var authType = AuthType.Basic;
 | 
			
		||||
 | 
			
		||||
        var user = userName.StartsWith("cn=") || userName.StartsWith("uid=") ? userName : "uid=" + userName + "," + AppConfiguration.Instance.LDAPConfig.LDAPUserQueryBase;
 | 
			
		||||
 | 
			
		||||
        //var connection = new LdapConnection(ldapServer)
 | 
			
		||||
        var connection = new LdapConnection(
 | 
			
		||||
            new LdapDirectoryIdentifier(this.Uri, this.Port)
 | 
			
		||||
            )
 | 
			
		||||
        {
 | 
			
		||||
            AuthType = authType,
 | 
			
		||||
            Credential = new(user, userPass)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // the default one is v2 (at least in that version), and it is unknown if v3
 | 
			
		||||
        // is actually needed, but at least Synology LDAP works only with v3,
 | 
			
		||||
        // and since our Exchange doesn't complain, let it be v3
 | 
			
		||||
        connection.SessionOptions.ProtocolVersion = 3;
 | 
			
		||||
 | 
			
		||||
        // this is for connecting via LDAPS (636 port). It should be working,
 | 
			
		||||
        // according to https://github.com/dotnet/runtime/issues/43890,
 | 
			
		||||
        // but it doesn't (at least with Synology DSM LDAP), although perhaps
 | 
			
		||||
        // for a different reason
 | 
			
		||||
        //connection.SessionOptions.SecureSocketLayer = true;
 | 
			
		||||
 | 
			
		||||
        connection.Bind();
 | 
			
		||||
 | 
			
		||||
        var request = new SearchRequest(targetOU, query, scope, attributesToQuery);
 | 
			
		||||
 | 
			
		||||
        var response = (System.DirectoryServices.Protocols.SearchResponse)connection.SendRequest(request);
 | 
			
		||||
 | 
			
		||||
        var userList = new List<LdapUser>();
 | 
			
		||||
 | 
			
		||||
        foreach (SearchResultEntry result in response.Entries)
 | 
			
		||||
        {
 | 
			
		||||
            userList.Add(new LdapUser()
 | 
			
		||||
            {
 | 
			
		||||
                UserId = result.Attributes["uid"][0].ToString(),
 | 
			
		||||
                FirstName = result.Attributes["givenName"][0].ToString(),
 | 
			
		||||
                LastName = result.Attributes["sn"][0].ToString(),
 | 
			
		||||
                Email = result.Attributes["mail"][0].ToString(),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        connection.Dispose();
 | 
			
		||||
 | 
			
		||||
        return userList;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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("/");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								song_of_the_day/Pages/Shared/SongPartialModel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								song_of_the_day/Pages/Shared/SongPartialModel.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
public class SongPartialModel
 | 
			
		||||
{
 | 
			
		||||
    public 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; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								song_of_the_day/Pages/Shared/UpdateInputText.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								song_of_the_day/Pages/Shared/UpdateInputText.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
@inherits Microsoft.AspNetCore.Components.Forms.InputText
 | 
			
		||||
<input @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />
 | 
			
		||||
@@ -1,4 +1,15 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
@{
 | 
			
		||||
    bool DoesUserHaveClaimedPhoneNumber()
 | 
			
		||||
    {
 | 
			
		||||
        using (var dci = DataContext.Instance)
 | 
			
		||||
        {
 | 
			
		||||
            var user = dci.Users?.Where(u => u.LdapUserName == User.Identity.Name);
 | 
			
		||||
            return user == null ? false : user.Any();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
@@ -7,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>
 | 
			
		||||
@@ -26,9 +37,26 @@
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</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" 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>
 | 
			
		||||
            <div class="container" style="min-height: auto; width: 400px;">
 | 
			
		||||
                <partial name="_LoginView" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
    </header>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
 
 | 
			
		||||
@@ -45,4 +45,4 @@ button.accept-policy {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  line-height: 60px;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								song_of_the_day/Pages/Shared/_LoginView.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								song_of_the_day/Pages/Shared/_LoginView.cshtml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
@using Microsoft.AspNetCore.Authentication
 | 
			
		||||
 | 
			
		||||
<div class="loginform">
 | 
			
		||||
    @if (this.User == null || this.User.Identity == null || !this.User.Identity.IsAuthenticated)
 | 
			
		||||
    {
 | 
			
		||||
        <form method="post" action="Auth/Login">
 | 
			
		||||
            <div>
 | 
			
		||||
                <label for="username">Username:</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <input name="username" type="text" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <label for="password">Password:</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <input name="password" type="password" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <input name="submit" type="submit" value="Login" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </form>
 | 
			
		||||
    }
 | 
			
		||||
    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, <a href="/User/@userId">@User.Identity.Name</a>!
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <input name="submit" type="submit" value="Logout" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </form>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										8
									
								
								song_of_the_day/Pages/Shared/_SongPartial.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								song_of_the_day/Pages/Shared/_SongPartial.cshtml
									
									
									
									
									
										Normal 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 />
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
@model List<SpotifyAPI.Web.FullTrack>
 | 
			
		||||
<div class="spotifySongSelector">
 | 
			
		||||
    @foreach(var track in Model)
 | 
			
		||||
    {
 | 
			
		||||
        <div class="songSelectorButton" onclick="clickHandler(this)">
 | 
			
		||||
            <div class="songName">@track.Name</div>
 | 
			
		||||
            <div class="artist">@track.Artists[0].Name</div>
 | 
			
		||||
            <div class="album">@track.Album.Name</div>
 | 
			
		||||
            <div class="id" hidden>@track.Id</div>
 | 
			
		||||
            <div class="albumCover">
 | 
			
		||||
                <img src="@track.Album.Images[0].Url" width="50px" height="50px"/>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    }
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    function clickHandler(t) {
 | 
			
		||||
        // remove previous selection
 | 
			
		||||
        $('.songSelectorButton').removeClass("selected");
 | 
			
		||||
        t.classList.add("selected");
 | 
			
		||||
        var idElement = t.getElementsByClassName("id")[0]
 | 
			
		||||
        console.log(idElement);
 | 
			
		||||
        SetSpotifyId(idElement.textContent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function initializeSelection() {
 | 
			
		||||
        var currentSpotifyId = GetSpotifyId();
 | 
			
		||||
        $('.songSelectorButton').each(function(i, e) {
 | 
			
		||||
            if(e.getElementsByClassName("id")[0].textContent == currentSpotifyId)
 | 
			
		||||
            {
 | 
			
		||||
                e.classList.add("selected");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initializeSelection();
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
.songSelectorButton {
 | 
			
		||||
  background-color: #e3e6e7;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.songSelectorButton:nth-child(2n) {
 | 
			
		||||
  background-color: #cacbce;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.songSelectorButton.selected {
 | 
			
		||||
  background-color: #a0c3e5;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  border-width: 2px;
 | 
			
		||||
  border-color: #285786;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								song_of_the_day/Pages/SongSubmission.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								song_of_the_day/Pages/SongSubmission.cshtml
									
									
									
									
									
										Normal 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>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										290
									
								
								song_of_the_day/Pages/SongSubmission.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								song_of_the_day/Pages/SongSubmission.cshtml.cs
									
									
									
									
									
										Normal 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); ;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 = "";
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								song_of_the_day/Pages/UnclaimedPhoneNumbers.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								song_of_the_day/Pages/UnclaimedPhoneNumbers.cshtml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
@page
 | 
			
		||||
@model UnclaimedPhoneNumbersModel
 | 
			
		||||
@{
 | 
			
		||||
    ViewData["Title"] = "Unclaimed Phone Numbers";
 | 
			
		||||
 | 
			
		||||
    var codeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
 | 
			
		||||
    var codeGenerated = codeService.IsCodeGeneratedForUser(User.Identity.Name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<div class="text-left">
 | 
			
		||||
    <table>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th>Phone Number</th>
 | 
			
		||||
            <th>Claim</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
        @foreach (var user in @Model.UnclaimedUsers)
 | 
			
		||||
        {
 | 
			
		||||
            var phone = user.SignalMemberId; var userId = user.UserId;
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>@phone</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <form method="post">
 | 
			
		||||
                        <input name="userIndex" value="@userId" type="hidden" /> 
 | 
			
		||||
                        <input type="submit" title="Claim" value="Claim" disabled="@codeGenerated" />
 | 
			
		||||
                    </form>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        }
 | 
			
		||||
    </table>   
 | 
			
		||||
    @if(codeGenerated)
 | 
			
		||||
    {
 | 
			
		||||
        <form method="post">
 | 
			
		||||
            <label for="code">Verification code:</label>
 | 
			
		||||
            <input type="text" id="code" name="code" />
 | 
			
		||||
            <input type="submit" title="Verify" value="Verify" asp-page-handler="SubmitCode" />
 | 
			
		||||
        </form>
 | 
			
		||||
    } 
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										66
									
								
								song_of_the_day/Pages/UnclaimedPhoneNumbers.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								song_of_the_day/Pages/UnclaimedPhoneNumbers.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.RazorPages;
 | 
			
		||||
using Microsoft.VisualBasic;
 | 
			
		||||
using SpotifyAPI.Web;
 | 
			
		||||
 | 
			
		||||
namespace sotd.Pages;
 | 
			
		||||
 | 
			
		||||
public class UnclaimedPhoneNumbersModel : PageModel
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<UserModel> _logger;
 | 
			
		||||
 | 
			
		||||
    public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        UnclaimedUsers = new List<User>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int userId { get; set; }
 | 
			
		||||
 | 
			
		||||
    [BindProperty]
 | 
			
		||||
    public List<User> UnclaimedUsers { get; set; }
 | 
			
		||||
 | 
			
		||||
    public void OnGet()
 | 
			
		||||
    {
 | 
			
		||||
        using (var dci = DataContext.Instance)
 | 
			
		||||
        {
 | 
			
		||||
            this.UnclaimedUsers = dci.Users == null ? new List<User>() : dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void OnPost(int userIndex)
 | 
			
		||||
    {
 | 
			
		||||
        using (var dci = DataContext.Instance)
 | 
			
		||||
        {
 | 
			
		||||
            var user = dci.Users.Find(userIndex);
 | 
			
		||||
            var claimCodeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
 | 
			
		||||
            claimCodeService.GenerateClaimCodeForUserAndNumber(HttpContext.User.Identity.Name, user.SignalMemberId);
 | 
			
		||||
            this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IActionResult OnPostSubmitCode(string code)
 | 
			
		||||
    {
 | 
			
		||||
        var claimCodeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
 | 
			
		||||
        var validatedNumber = claimCodeService.ValidateClaimCodeForUser(code, HttpContext.User.Identity.Name);
 | 
			
		||||
        if (!string.IsNullOrEmpty(validatedNumber))
 | 
			
		||||
        {
 | 
			
		||||
            using (var dci = DataContext.Instance)
 | 
			
		||||
            {
 | 
			
		||||
                var user = dci.Users.Where(u => u.SignalMemberId == validatedNumber).FirstOrDefault();
 | 
			
		||||
                if (user == default(User))
 | 
			
		||||
                {
 | 
			
		||||
                    throw new Exception("User with specified phone number not found!");
 | 
			
		||||
                }
 | 
			
		||||
                user.LdapUserName = HttpContext.User.Identity.Name;
 | 
			
		||||
                dci.SaveChanges();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception("Invalid code provided!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return RedirectToPage("/");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,65 @@
 | 
			
		||||
 | 
			
		||||
using Scalar.AspNetCore;
 | 
			
		||||
using Microsoft.AspNetCore.OpenApi;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
SignalIntegration.Instance = new SignalIntegration(AppConfiguration.Instance.SignalAPIEndpointUri,
 | 
			
		||||
                                                    int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort),
 | 
			
		||||
                                                    AppConfiguration.Instance.HostPhoneNumber);
 | 
			
		||||
using Microsoft.AspNetCore.Authentication.Cookies;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using SpotifyAPI.Web;
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
 | 
			
		||||
var builder = WebApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    {
 | 
			
		||||
        var foundUser = dci.Users.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
 | 
			
		||||
        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,
 | 
			
		||||
                SignalMemberId = memberId,
 | 
			
		||||
                NickName = string.Empty,
 | 
			
		||||
                IsIntroduced = false
 | 
			
		||||
                IsIntroduced = false,
 | 
			
		||||
                LdapUserName = string.Empty,
 | 
			
		||||
                AssociationInProgress = false,
 | 
			
		||||
                WasChosenForSuggestionThisRound = false,
 | 
			
		||||
                LikedSongs = new List<Song>()
 | 
			
		||||
            };
 | 
			
		||||
            dci.Users?.Add(newUser);
 | 
			
		||||
            needsSaving = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -41,17 +70,66 @@ userCheckTimer.OnOccurence += async (s, ea) =>
 | 
			
		||||
    }
 | 
			
		||||
    await dci.DisposeAsync();
 | 
			
		||||
};
 | 
			
		||||
userCheckTimer.Start();
 | 
			
		||||
 | 
			
		||||
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");
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            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;
 | 
			
		||||
    var introUsers = dci.Users.Where(u => !u.IsIntroduced);
 | 
			
		||||
    var introUsers = dci.Users?.Where(u => !u.IsIntroduced);
 | 
			
		||||
    if (introUsers == null)
 | 
			
		||||
    {
 | 
			
		||||
        await dci.DisposeAsync();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
@@ -62,27 +140,136 @@ userIntroTimer.OnOccurence += async (s, ea) =>
 | 
			
		||||
    }
 | 
			
		||||
    await dci.DisposeAsync();
 | 
			
		||||
};
 | 
			
		||||
userIntroTimer.Start();
 | 
			
		||||
 | 
			
		||||
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 luckyUser = await dci.Users.ElementAtAsync((new Random()).Next(await dci.Users.CountAsync()));
 | 
			
		||||
 | 
			
		||||
    var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();
 | 
			
		||||
 | 
			
		||||
    if (lastSong != null && lastSong.Date > DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
 | 
			
		||||
    {
 | 
			
		||||
        logger.LogWarning("Skipping pick of the day today!");
 | 
			
		||||
        await dci.DisposeAsync();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
 | 
			
		||||
    {
 | 
			
		||||
        logger.LogError("Unable to properly initialize DB context!");
 | 
			
		||||
        await dci.DisposeAsync();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
 | 
			
		||||
    if (!potentialUsers.Any())
 | 
			
		||||
    {
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
    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)
 | 
			
		||||
    {
 | 
			
		||||
        logger.LogError("Unable to determine today's lucky user!");
 | 
			
		||||
        await dci.DisposeAsync();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    var userName = string.IsNullOrEmpty(luckyUser.NickName) ? luckyUser.Name : luckyUser.NickName;
 | 
			
		||||
    SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
 | 
			
		||||
    SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", luckyUser.SignalMemberId);
 | 
			
		||||
    var suggestion = await dci.SuggestionHelpers.ElementAtAsync((new Random()).Next(await dci.SuggestionHelpers.CountAsync()));
 | 
			
		||||
    SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", luckyUser.SignalMemberId);
 | 
			
		||||
    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 newSongSuggestion = new SongSuggestion()
 | 
			
		||||
    {
 | 
			
		||||
        User = luckyUser,
 | 
			
		||||
        SuggestionHelper = suggestion,
 | 
			
		||||
        UserHasSubmitted = false,
 | 
			
		||||
        HasUsedSuggestion = false,
 | 
			
		||||
        Date = DateTime.Today.ToUniversalTime()
 | 
			
		||||
    };
 | 
			
		||||
    if (luckyUser.SignalMemberId is string signalId)
 | 
			
		||||
    {
 | 
			
		||||
        var result = await dci.SongSuggestions.AddAsync(newSongSuggestion);
 | 
			
		||||
        newSongSuggestion = result.Entity;
 | 
			
		||||
        luckyUser.WasChosenForSuggestionThisRound = true;
 | 
			
		||||
        await dci.SaveChangesAsync();
 | 
			
		||||
        var signalIntegration = app.Services.GetService<SignalIntegration>();
 | 
			
		||||
        await signalIntegration.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
 | 
			
		||||
        await signalIntegration.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();
 | 
			
		||||
 | 
			
		||||
// Add services to the container.
 | 
			
		||||
builder.Services.AddRazorPages();
 | 
			
		||||
builder.Services.AddOpenApi();
 | 
			
		||||
var startUserAssociationProcess = async (User userToAssociate) =>
 | 
			
		||||
{
 | 
			
		||||
    if (userToAssociate.SignalMemberId is string 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);
 | 
			
		||||
    }
 | 
			
		||||
};    
 | 
			
		||||
 | 
			
		||||
var app = builder.Build();
 | 
			
		||||
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)
 | 
			
		||||
    {
 | 
			
		||||
        logger.LogError("Unable to properly initialize DB context!");
 | 
			
		||||
        await dci.DisposeAsync();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    var nonAssociatedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName) && !u.AssociationInProgress);
 | 
			
		||||
    var needsSaving = false;
 | 
			
		||||
    foreach (var user in nonAssociatedUsers)
 | 
			
		||||
    {
 | 
			
		||||
        user.AssociationInProgress = true;
 | 
			
		||||
 | 
			
		||||
        await startUserAssociationProcess(user);
 | 
			
		||||
        user.IsIntroduced = true;
 | 
			
		||||
        needsSaving = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (needsSaving)
 | 
			
		||||
    {
 | 
			
		||||
        await dci.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
    await dci.DisposeAsync();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
 | 
			
		||||
// only start interaction timers in production builds
 | 
			
		||||
// for local/development testing we want those disabled
 | 
			
		||||
if (!app.Environment.IsDevelopment())
 | 
			
		||||
{
 | 
			
		||||
    logger.LogTrace("Starting timer for scheduled processes.");
 | 
			
		||||
    userCheckTimer.Start();
 | 
			
		||||
    userIntroTimer.Start();
 | 
			
		||||
    pickOfTheDayTimer.Start();
 | 
			
		||||
    ldapAssociationTimer.Start();
 | 
			
		||||
    likePlaylistCheckTimer.Start();
 | 
			
		||||
}
 | 
			
		||||
else
 | 
			
		||||
{
 | 
			
		||||
    logger.LogTrace("This is a debug build - scheduled processes are disabled.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Configure the HTTP request pipeline.
 | 
			
		||||
if (!app.Environment.IsDevelopment())
 | 
			
		||||
@@ -102,5 +289,36 @@ app.UseAuthorization();
 | 
			
		||||
app.MapStaticAssets();
 | 
			
		||||
app.MapRazorPages()
 | 
			
		||||
   .WithStaticAssets();
 | 
			
		||||
app.MapControllerRoute(
 | 
			
		||||
    name: "login",
 | 
			
		||||
    pattern: "{controller=Auth}/{action=Login}"
 | 
			
		||||
);
 | 
			
		||||
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)));
 | 
			
		||||
 | 
			
		||||
app.Run();
 | 
			
		||||
							
								
								
									
										31
									
								
								song_of_the_day/SongValidators/Base64UrlImageBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								song_of_the_day/SongValidators/Base64UrlImageBuilder.cs
									
									
									
									
									
										Normal 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}";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								song_of_the_day/SongValidators/ISongValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								song_of_the_day/SongValidators/ISongValidator.cs
									
									
									
									
									
										Normal 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();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								song_of_the_day/SongValidators/NavidromeValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								song_of_the_day/SongValidators/NavidromeValidator.cs
									
									
									
									
									
										Normal 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; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								song_of_the_day/SongValidators/SongResolver.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								song_of_the_day/SongValidators/SongResolver.cs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								song_of_the_day/SongValidators/SongValidatorBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								song_of_the_day/SongValidators/SongValidatorBase.cs
									
									
									
									
									
										Normal 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 : "";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								song_of_the_day/SongValidators/SpotifyValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								song_of_the_day/SongValidators/SpotifyValidator.cs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								song_of_the_day/SongValidators/YoutubeMusicValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								song_of_the_day/SongValidators/YoutubeMusicValidator.cs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								song_of_the_day/SongValidators/YoutubeValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								song_of_the_day/SongValidators/YoutubeValidator.cs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								song_of_the_day/SpotifyIntegration/PlayListSynchronizer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								song_of_the_day/SpotifyIntegration/PlayListSynchronizer.cs
									
									
									
									
									
										Normal 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										293
									
								
								song_of_the_day/SpotifyIntegration/SpotifyApiClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								song_of_the_day/SpotifyIntegration/SpotifyApiClient.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
			
		||||
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;
 | 
			
		||||
            if (!string.IsNullOrEmpty(oAuthResponse.RefreshToken))
 | 
			
		||||
            {
 | 
			
		||||
                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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
0.1.0
 | 
			
		||||
0.6.10
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,11 @@
 | 
			
		||||
  "DetailedErrors": true,
 | 
			
		||||
  "Logging": {
 | 
			
		||||
    "LogLevel": {
 | 
			
		||||
      "Default": "Information",
 | 
			
		||||
      "Microsoft.AspNetCore": "Warning"
 | 
			
		||||
      "Default": "Trace",
 | 
			
		||||
      "Microsoft.AspNetCore": "Warning",
 | 
			
		||||
      "SongResolver": "Trace",
 | 
			
		||||
      "SignalIntegration": "Trace",
 | 
			
		||||
      "SongOfTheDay": "Trace"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "Logging": {
 | 
			
		||||
    "LogLevel": {
 | 
			
		||||
      "Default": "Information",
 | 
			
		||||
      "Microsoft.AspNetCore": "Warning"
 | 
			
		||||
      "Default": "Trace",
 | 
			
		||||
      "Microsoft.AspNetCore": "Warning",
 | 
			
		||||
      "SongResolver": "Information",
 | 
			
		||||
      "SignalIntegration": "Information",
 | 
			
		||||
      "SongOfTheDay": "Trace"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "AllowedHosts": "*"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,19 +6,25 @@
 | 
			
		||||
    <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="CronTimer" Version="2.0.0" />
 | 
			
		||||
    <PackageReference Include="SpotifyAPI.Web" Version="7.2.1" />
 | 
			
		||||
    <PackageReference Include="YouTubeMusicAPI" Version="2.2.8" />
 | 
			
		||||
    <PackageReference Include="System.DirectoryServices.Protocols" Version="*" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <OpenApiReference Include="swagger.json" SourceUrl="https://bbernhard.github.io/signal-cli-rest-api/src/docs/swagger.json" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <ProjectReference Include="..\CronTimer\CronTimer.csproj" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -2423,6 +2423,9 @@
 | 
			
		||||
                "edit_timestamp": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                },
 | 
			
		||||
                "link_preview": {
 | 
			
		||||
                    "$ref": "#/definitions/data.LinkPreviewType"
 | 
			
		||||
                },
 | 
			
		||||
                "mentions": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "items": {
 | 
			
		||||
@@ -2817,6 +2820,23 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "data.LinkPreviewType": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "base64_thumbnail": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "description": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "title": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "url": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "data.MessageMention": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user