Compare commits
58 Commits
0.1.6
...
dbd83ebb6a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
@@ -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,9 +86,9 @@ start() {
|
||||
echo "New version: $new_version"
|
||||
|
||||
gitchangelog | grep -v "[rR]elease:" > HISTORY.md
|
||||
git add song_of_the_day/VERSION
|
||||
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 '-' '_')
|
||||
@@ -61,5 +57,5 @@ jobs:
|
||||
run: |
|
||||
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
|
||||
|
||||
# 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"]
|
280
HISTORY.md
280
HISTORY.md
@@ -5,10 +5,290 @@ Changelog
|
||||
(unreleased)
|
||||
------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Save DateTime as UTC, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
|
||||
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)
|
||||
------------------
|
||||
|
||||
|
||||
0.1.8 (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)
|
||||
------------------
|
||||
|
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>
|
4
song_of_the_day/.editorconfig
Normal file
4
song_of_the_day/.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
# CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
|
||||
dotnet_diagnostic.CS8981.severity = none
|
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);
|
||||
}
|
15
song_of_the_day/Auth/LdapAuthenticationService.cs
Normal file
15
song_of_the_day/Auth/LdapAuthenticationService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
public class LdapAuthenticationService : IAuthenticationService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public LdapAuthenticationService(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public bool Authenticate(string username, string password)
|
||||
{
|
||||
var ldapInstance = LdapIntegration.Instance;
|
||||
return ldapInstance == null ? false : ldapInstance.TestLogin(username, password);
|
||||
}
|
||||
}
|
58
song_of_the_day/Auth/PhoneClaimCodeProviderService.cs
Normal file
58
song_of_the_day/Auth/PhoneClaimCodeProviderService.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
public class PhoneClaimCodeProviderService
|
||||
{
|
||||
private Dictionary<string, string> _phoneClaimCodes;
|
||||
private Dictionary<string, string> _phoneClaimNumbers;
|
||||
|
||||
public PhoneClaimCodeProviderService()
|
||||
{
|
||||
_phoneClaimCodes = new Dictionary<string, string>();
|
||||
_phoneClaimNumbers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
private static Random random = new Random();
|
||||
|
||||
private static string RandomString(int length)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
await SignalIntegration.Instance.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,23 @@ 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"
|
||||
};
|
||||
}
|
||||
|
||||
public string SignalAPIEndpointUri
|
||||
@@ -68,8 +85,28 @@ 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;
|
||||
}
|
||||
}
|
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("/");
|
||||
}
|
||||
}
|
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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -30,15 +30,12 @@ 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<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("SongId");
|
||||
@@ -57,16 +54,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,11 +89,9 @@ namespace song_of_the_day.DataMigrations
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
@@ -101,21 +107,27 @@ 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<bool>("WasChosenForSuggestionThisRound")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("UserId");
|
||||
|
||||
b.ToTable("Users");
|
||||
@@ -125,18 +137,22 @@ namespace song_of_the_day.DataMigrations
|
||||
{
|
||||
b.HasOne("Song", "Song")
|
||||
.WithMany()
|
||||
.HasForeignKey("SongId")
|
||||
.HasForeignKey("SongId");
|
||||
|
||||
b.HasOne("SuggestionHelper", "SuggestionHelper")
|
||||
.WithMany()
|
||||
.HasForeignKey("SuggestionHelperId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("Song");
|
||||
|
||||
b.Navigation("SuggestionHelper");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
|
@@ -6,4 +6,6 @@ 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; }
|
||||
}
|
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,7 @@ 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; }
|
||||
}
|
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; }
|
||||
}
|
125
song_of_the_day/LDAPIntegration/LdapIntegration.cs
Normal file
125
song_of_the_day/LDAPIntegration/LdapIntegration.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using song_of_the_day;
|
||||
using System.DirectoryServices.Protocols;
|
||||
using System.Linq;
|
||||
|
||||
public class LdapIntegration
|
||||
{
|
||||
public static LdapIntegration? Instance;
|
||||
|
||||
private readonly string[] attributesToQuery = new string[]
|
||||
{
|
||||
"uid",
|
||||
"givenName",
|
||||
"sn",
|
||||
"mail"
|
||||
};
|
||||
|
||||
public LdapIntegration(string uri, int port, string adminBind, string adminPass)
|
||||
{
|
||||
this.Uri = uri;
|
||||
this.Port = port;
|
||||
this.AdminBind = adminBind;
|
||||
this.AdminPass = adminPass;
|
||||
}
|
||||
|
||||
private string Uri { get; set; }
|
||||
|
||||
private int Port { get; set; }
|
||||
|
||||
private string AdminBind { get; set; }
|
||||
|
||||
private string AdminPass { 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;
|
||||
}
|
||||
}
|
@@ -7,8 +7,13 @@ public class SignalIntegration
|
||||
{
|
||||
public static SignalIntegration? Instance;
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
public SignalIntegration(string uri, int port, string phoneNumber)
|
||||
{
|
||||
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
|
||||
this.logger = factory.CreateLogger("SignalIntegration");
|
||||
|
||||
var http = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(uri + ":" + port)
|
||||
@@ -24,17 +29,19 @@ public class SignalIntegration
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +59,7 @@ public class SignalIntegration
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Exception (SendMessageToGroupAsync): " + ex.Message);
|
||||
logger.LogError("Exception (SendMessageToGroupAsync): " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +77,7 @@ public class SignalIntegration
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Exception (SendMessageToUserAsync): " + ex.Message);
|
||||
logger.LogError("Exception (SendMessageToUserAsync): " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +104,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 +124,7 @@ public class SignalIntegration
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Exception (GetContactAsync): " + ex.Message);
|
||||
logger.LogError("Exception (GetContactAsync): " + ex.Message);
|
||||
return new ListContactsResponse();
|
||||
}
|
||||
}
|
||||
|
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.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -26,9 +37,24 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a>
|
||||
</li>
|
||||
@if (this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber())
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
|
||||
</li>
|
||||
}
|
||||
@if (this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-page="/SubmitSongs">Submit Songs</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" style="min-height: auto; width: 400px;">
|
||||
<partial name="_LoginView" />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
|
36
song_of_the_day/Pages/Shared/_LoginView.cshtml
Normal file
36
song_of_the_day/Pages/Shared/_LoginView.cshtml
Normal file
@@ -0,0 +1,36 @@
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
|
||||
<div class="loginform">
|
||||
@if (!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
|
||||
{
|
||||
<form method="post" action="Auth/Logout">
|
||||
<div>
|
||||
Welcome, @User.Identity.Name!
|
||||
</div>
|
||||
<div>
|
||||
<input name="submit" type="submit" value="Logout" />
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
</div>
|
7
song_of_the_day/Pages/Shared/_SongPartial.cshtml
Normal file
7
song_of_the_day/Pages/Shared/_SongPartial.cshtml
Normal file
@@ -0,0 +1,7 @@
|
||||
@model Song
|
||||
<label asp-for="Name">Name:</label>
|
||||
<input asp-for="Name" />
|
||||
<label asp-for="Artist">Artist:</label>
|
||||
<input asp-for="Artist" />
|
||||
<label asp-for="SpotifyId">Spotify ID:</label>
|
||||
<input asp-for="SpotifyId" />
|
25
song_of_the_day/Pages/SubmitSongs.cshtml
Normal file
25
song_of_the_day/Pages/SubmitSongs.cshtml
Normal file
@@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model SubmitSongsModel
|
||||
@{
|
||||
ViewData["Title"] = "Submit Songs";
|
||||
}
|
||||
|
||||
<div class="text-left">
|
||||
<form method="post">
|
||||
<label asp-for="SubmitUrl" >Song Url:</label>
|
||||
<input asp-for="SubmitUrl" oninput="Update(this)" />
|
||||
</form>
|
||||
<form method="post">
|
||||
<div id="songdata">
|
||||
<partial name="_SongPartial" model="@Model.SongData" />
|
||||
</div>
|
||||
<input type="submit" title="Submit" value="Submit" disabled="CanSubmit" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function Update(t) {
|
||||
var url = '?handler=Update&&SubmitUrl=' + $(t).val();
|
||||
$('#songdata').load(url)
|
||||
}
|
||||
</script>
|
71
song_of_the_day/Pages/SubmitSongs.cshtml.cs
Normal file
71
song_of_the_day/Pages/SubmitSongs.cshtml.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.VisualBasic;
|
||||
|
||||
namespace sotd.Pages;
|
||||
|
||||
public class SubmitSongsModel : PageModel
|
||||
{
|
||||
private readonly ILogger<UserModel> _logger;
|
||||
|
||||
private SongResolver songResolver;
|
||||
|
||||
private string _submitUrl;
|
||||
|
||||
public SubmitSongsModel(ILogger<UserModel> logger, SongResolver songResolver)
|
||||
{
|
||||
_logger = logger;
|
||||
this.songResolver = songResolver;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public bool IsValidUrl { get; set; } = true;
|
||||
|
||||
[BindProperty]
|
||||
public string SubmitUrl {
|
||||
get {
|
||||
return _submitUrl;
|
||||
}
|
||||
set {
|
||||
_submitUrl = value.ToString();
|
||||
Uri? newValue = default;
|
||||
try {
|
||||
newValue = new Uri(_submitUrl);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
IsValidUrl = false;
|
||||
return;
|
||||
}
|
||||
|
||||
IsValidUrl = true;
|
||||
|
||||
if(this.songResolver.CanValidate(newValue))
|
||||
{
|
||||
this.SongData = this.songResolver.ResolveSongAsync(newValue).Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public bool CanSubmit { get {
|
||||
return !string.IsNullOrEmpty(SongData?.Artist) && ! string.IsNullOrEmpty(SongData?.Name);
|
||||
} }
|
||||
|
||||
[BindProperty]
|
||||
public Song SongData { get; set; }
|
||||
|
||||
public void OnPost()
|
||||
{
|
||||
// Todo implement save submission
|
||||
var x = SongData.Name;
|
||||
}
|
||||
|
||||
public IActionResult OnGetUpdate()
|
||||
{
|
||||
var songUrl = Request.Query["SubmitUrl"];
|
||||
this.SubmitUrl = songUrl.ToString();
|
||||
return Partial("_SongPartial", SongData);;
|
||||
}
|
||||
}
|
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>
|
64
song_of_the_day/Pages/UnclaimedPhoneNumbers.cshtml.cs
Normal file
64
song_of_the_day/Pages/UnclaimedPhoneNumbers.cshtml.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.VisualBasic;
|
||||
|
||||
namespace sotd.Pages;
|
||||
|
||||
public class UnclaimedPhoneNumbersModel : PageModel
|
||||
{
|
||||
private readonly ILogger<UserModel> _logger;
|
||||
|
||||
public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public int userId { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public List<User> UnclaimedUsers { get; set; }
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
using (var dci = DataContext.Instance)
|
||||
{
|
||||
this.UnclaimedUsers = 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("/");
|
||||
}
|
||||
}
|
@@ -2,13 +2,26 @@
|
||||
using Scalar.AspNetCore;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using System.DirectoryServices.Protocols;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
SignalIntegration.Instance = new SignalIntegration(AppConfiguration.Instance.SignalAPIEndpointUri,
|
||||
int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort),
|
||||
AppConfiguration.Instance.HostPhoneNumber);
|
||||
|
||||
LdapIntegration.Instance = new LdapIntegration(AppConfiguration.Instance.LDAPConfig.LDAPserver,
|
||||
AppConfiguration.Instance.LDAPConfig.Port,
|
||||
AppConfiguration.Instance.LDAPConfig.Username,
|
||||
AppConfiguration.Instance.LDAPConfig.Password);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
|
||||
var logger = factory.CreateLogger("SongResolver");
|
||||
|
||||
logger.LogTrace("Setting up user check timer");
|
||||
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
|
||||
userCheckTimer.OnOccurence += async (s, ea) =>
|
||||
{
|
||||
@@ -17,20 +30,24 @@ userCheckTimer.OnOccurence += async (s, ea) =>
|
||||
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}");
|
||||
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,
|
||||
};
|
||||
dci.Users?.Add(newUser);
|
||||
needsSaving = true;
|
||||
}
|
||||
}
|
||||
@@ -41,13 +58,19 @@ userCheckTimer.OnOccurence += async (s, ea) =>
|
||||
}
|
||||
await dci.DisposeAsync();
|
||||
};
|
||||
userCheckTimer.Start();
|
||||
|
||||
logger.LogTrace("Setting up user intro timer");
|
||||
var userIntroTimer = new CronTimer("*/1 * * * *", "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)
|
||||
{
|
||||
@@ -62,28 +85,133 @@ userIntroTimer.OnOccurence += async (s, ea) =>
|
||||
}
|
||||
await dci.DisposeAsync();
|
||||
};
|
||||
userIntroTimer.Start();
|
||||
|
||||
|
||||
logger.LogTrace("Setting up pick of the day timer");
|
||||
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "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)
|
||||
{
|
||||
await dci.SongSuggestions.AddAsync(newSongSuggestion);
|
||||
await dci.SaveChangesAsync();
|
||||
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
|
||||
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*");
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId);
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId);
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId);
|
||||
}
|
||||
await dci.DisposeAsync();
|
||||
};
|
||||
|
||||
var startUserAssociationProcess = async (User userToAssociate) =>
|
||||
{
|
||||
if (userToAssociate.SignalMemberId is string signalId)
|
||||
{
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId);
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId);
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId);
|
||||
await SignalIntegration.Instance.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId);
|
||||
}
|
||||
};
|
||||
|
||||
logger.LogTrace("Setting up LdapAssociation timer");
|
||||
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "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();
|
||||
};
|
||||
pickOfTheDayTimer.Start();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/Auth/Login";
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<LdapAuthenticationService>();
|
||||
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
|
||||
builder.Services.AddSingleton<SongResolver>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// only start interaction timers in production builds
|
||||
// for local/development testing we want those disabled
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
userCheckTimer.Start();
|
||||
userIntroTimer.Start();
|
||||
pickOfTheDayTimer.Start();
|
||||
ldapAssociationTimer.Start();
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -102,5 +230,15 @@ 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("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
|
||||
string.Join("\n", endpointSources.SelectMany(source => source.Endpoints)));
|
||||
|
||||
app.Run();
|
8
song_of_the_day/SongValidators/ISongValidator.cs
Normal file
8
song_of_the_day/SongValidators/ISongValidator.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
public interface ISongValidator
|
||||
{
|
||||
Task<Song> ValidateAsync(Uri songUri);
|
||||
|
||||
bool CanValidateUri(Uri songUri);
|
||||
|
||||
Task<bool> CanExtractSongMetadataAsync(Uri songUri);
|
||||
}
|
66
song_of_the_day/SongValidators/SongResolver.cs
Normal file
66
song_of_the_day/SongValidators/SongResolver.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class SongResolver
|
||||
{
|
||||
private readonly IEnumerable<ISongValidator> _songValidators;
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
public SongResolver()
|
||||
{
|
||||
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
|
||||
this.logger = factory.CreateLogger("SongResolver");
|
||||
|
||||
this._songValidators = new List<ISongValidator>();
|
||||
|
||||
foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base")))) {
|
||||
if (Activator.CreateInstance(mytype) 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 (validator.CanValidateUri(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.CanValidateUri(songUri))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
16
song_of_the_day/SongValidators/SongValidatorBase.cs
Normal file
16
song_of_the_day/SongValidators/SongValidatorBase.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public abstract class SongValidatorBase : ISongValidator
|
||||
{
|
||||
public abstract Task<Song> ValidateAsync(Uri songUri);
|
||||
|
||||
public abstract Task<bool> CanExtractSongMetadataAsync(Uri songUri);
|
||||
|
||||
public abstract bool CanValidateUri(Uri songUri);
|
||||
|
||||
protected string LookupSpotifyId(string songName, string songArtist)
|
||||
{
|
||||
// TODO: Implement Spotify ID lookup logic
|
||||
return songName + " by " + songArtist;
|
||||
}
|
||||
}
|
40
song_of_the_day/SongValidators/SpotifyValidator.cs
Normal file
40
song_of_the_day/SongValidators/SpotifyValidator.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
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_-]+)?$";
|
||||
|
||||
private SpotifyApiClient spotifyApiClient;
|
||||
|
||||
public SpotifyValidator()
|
||||
{
|
||||
spotifyApiClient = new SpotifyApiClient();
|
||||
}
|
||||
|
||||
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||
{
|
||||
return this.CanValidateUri(songUri);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
12
song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
Normal file
12
song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public abstract class UriBasedSongValidatorBase : SongValidatorBase
|
||||
{
|
||||
public abstract string UriValidatorRegex { get; }
|
||||
|
||||
public override bool CanValidateUri(Uri songUri)
|
||||
{
|
||||
var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
|
||||
return regexp.Match(songUri.ToString()).Success;
|
||||
}
|
||||
}
|
43
song_of_the_day/SongValidators/YoutubeMusicValidator.cs
Normal file
43
song_of_the_day/SongValidators/YoutubeMusicValidator.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
|
||||
public class YoutubeMusicValidator : UriBasedSongValidatorBase
|
||||
{
|
||||
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
|
||||
|
||||
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||
{
|
||||
return this.CanValidateUri(songUri);
|
||||
}
|
||||
|
||||
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||
{
|
||||
var title = string.Empty;
|
||||
var artist = string.Empty;
|
||||
|
||||
using(HttpClient httpClient = new HttpClient())
|
||||
{
|
||||
var response = await httpClient.GetAsync(songUri);
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
|
||||
{
|
||||
// document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("song-title")[0].innerHTML
|
||||
title = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".song-title")?.InnerHtml;
|
||||
// document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("byline")[0].innerHTML
|
||||
artist = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".byline")?.InnerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
var song = new Song
|
||||
{
|
||||
Name = title,
|
||||
Artist = artist,
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.YouTube,
|
||||
SpotifyId = this.LookupSpotifyId(title, artist)
|
||||
};
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
55
song_of_the_day/SongValidators/YoutubeValidator.cs
Normal file
55
song_of_the_day/SongValidators/YoutubeValidator.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Dom;
|
||||
|
||||
public class YoutubeValidator : UriBasedSongValidatorBase
|
||||
{
|
||||
public override string UriValidatorRegex => @"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
|
||||
|
||||
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||
{
|
||||
using(HttpClient httpClient = new HttpClient())
|
||||
{
|
||||
var response = await httpClient.GetAsync(songUri);
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
|
||||
{
|
||||
var documentContents = (document.ChildNodes[1] as HtmlElement).InnerHtml;
|
||||
var titleElement = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0];
|
||||
var artistParentElement = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0];
|
||||
|
||||
return titleElement != null && artistParentElement != null && artistParentElement.Children.Length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||
{
|
||||
var title = string.Empty;
|
||||
var artist = string.Empty;
|
||||
|
||||
using(HttpClient httpClient = new HttpClient())
|
||||
{
|
||||
var response = await httpClient.GetAsync(songUri);
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
|
||||
{
|
||||
title = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0]?.InnerHtml;
|
||||
artist = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0]?.Children[0]?.InnerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
var song = new Song
|
||||
{
|
||||
Name = title,
|
||||
Artist = artist,
|
||||
Url = songUri.ToString(),
|
||||
Provider = SongProvider.YouTube,
|
||||
SpotifyId = this.LookupSpotifyId(title, artist)
|
||||
};
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
44
song_of_the_day/SpotifyIntegration/SpotifyClient.cs
Normal file
44
song_of_the_day/SpotifyIntegration/SpotifyClient.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
public class SpotifyApiClient
|
||||
{
|
||||
private SpotifyClient _spotifyClient;
|
||||
|
||||
public SpotifyApiClient()
|
||||
{
|
||||
var config = SpotifyClientConfig.CreateDefault()
|
||||
.WithAuthenticator(new ClientCredentialsAuthenticator(
|
||||
AppConfiguration.Instance.SpotifyClientId,
|
||||
AppConfiguration.Instance.SpotifyClientSecret));
|
||||
|
||||
_spotifyClient = new SpotifyClient(config);
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +1 @@
|
||||
0.1.4
|
||||
0.3.3
|
||||
|
@@ -6,19 +6,24 @@
|
||||
<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="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>
|
Reference in New Issue
Block a user