mirror of
https://github.com/Lidarr/Lidarr
synced 2025-12-06 16:33:52 +01:00
Compare commits
48 commits
v2.14.3.47
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f79c0189 | ||
|
|
3023eabad0 | ||
|
|
563ffee615 | ||
|
|
350860e524 | ||
|
|
65802559cb | ||
|
|
9c9ebbca3f | ||
|
|
6bcead8bf6 | ||
|
|
1992127e91 | ||
|
|
3e2858439f | ||
|
|
505df12def | ||
|
|
96e5a4df2f | ||
|
|
7cdb4e4b52 | ||
|
|
5d141358da | ||
|
|
d5984d7386 | ||
|
|
87d46be67a | ||
|
|
8cd45394f3 | ||
|
|
5e683145e3 | ||
|
|
61c23de168 | ||
|
|
826b8b5933 | ||
|
|
1a4c1b6db5 | ||
|
|
96328b8d95 | ||
|
|
80ea0bd0a8 | ||
|
|
985ecd31e8 | ||
|
|
0501e61565 | ||
|
|
cce7ffa6ac | ||
|
|
ec9625f2b3 | ||
|
|
4baba902ea | ||
|
|
71ea85681f | ||
|
|
52a5034718 | ||
|
|
e3af1b682d | ||
|
|
5582e9ae27 | ||
|
|
d6d1461321 | ||
|
|
b574e403ce | ||
|
|
50e4725c87 | ||
|
|
944970a1f2 | ||
|
|
48f42fdcbd | ||
|
|
3da5d0bf0c | ||
|
|
3e5e3d9cd3 | ||
|
|
4143c59297 | ||
|
|
eb7e77b8e2 | ||
|
|
aaf025033e | ||
|
|
a8a87c00d7 | ||
|
|
4805dfc769 | ||
|
|
95089080f4 | ||
|
|
6adbbf81ed | ||
|
|
28811ace36 | ||
|
|
b8121759a7 | ||
|
|
477d0b70d0 |
128 changed files with 1694 additions and 1207 deletions
|
|
@ -2,7 +2,7 @@
|
|||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Lidarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
|
|
|
|||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -10,7 +10,7 @@
|
|||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Lidarr",
|
||||
"program": "${workspaceFolder}/_output/net8.0/Lidarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ Setup guides, [FAQ](/lidarr/faq), the more information we have on the [wiki](htt
|
|||
|
||||
# Development
|
||||
|
||||
Lidarr is written in C# (backend) and JS (frontend). The backend is built on the .NET6 (and _soon_ .NET8) framework, while the frontend utilizes Reactjs.
|
||||
Lidarr is written in C# (backend) and JS (frontend). The backend is built on the .NET 8 framework, while the frontend utilizes Reactjs.
|
||||
|
||||
## Tools required
|
||||
|
||||
- Visual Studio 2022 or higher is recommended (<https://www.visualstudio.com/vs/>). The community version is free and works (<https://www.visualstudio.com/downloads/>).
|
||||
|
||||
> VS 2022 V17.0 or higher is recommended as it includes the .NET6 SDK
|
||||
> VS 2022 V17.0 or higher is recommended as it includes the .NET 8 SDK
|
||||
{.is-info}
|
||||
|
||||
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||
|
|
@ -24,7 +24,7 @@ Lidarr is written in C# (backend) and JS (frontend). The backend is built on the
|
|||
- **20** (any minor or patch version within this)
|
||||
{.grid-list}
|
||||
|
||||
> The Application will **NOT** run on older versions such as `18.x`, `16.x` or any version below 20.0! Due to a dependency issue, it will also not run on `21.x` and is untested on other verisons.
|
||||
> The Application will **NOT** run on older versions such as `18.x`, `16.x` or any version below 20.0! Due to a dependency issue, it will also not run on `21.x` and is untested on other versions.
|
||||
{.is-warning}
|
||||
|
||||
- [Yarn](https://yarnpkg.com/getting-started/install) is required to build the frontend
|
||||
|
|
@ -60,7 +60,7 @@ The backend solution is most easily built and ran in Visual Studio or Rider, how
|
|||
|
||||
#### Visual Studio
|
||||
|
||||
> Ensure startup project is set to `Lidarr.Console` and framework to `net6.0`
|
||||
> Ensure startup project is set to `Lidarr.Console` and framework to `net8.0`
|
||||
{.is-info}
|
||||
|
||||
1. First `Build` the solution in Visual Studio, this will ensure all projects are correctly built and dependencies restored
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.14.3'
|
||||
majorVersion: '3.1.1'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.427'
|
||||
dotnetVersion: '8.0.405'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-22.04'
|
||||
macImage: 'macOS-13'
|
||||
windowsImage: 'windows-2025'
|
||||
linuxImage: 'ubuntu-24.04'
|
||||
macImage: 'macOS-15'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
|
@ -106,7 +106,7 @@ stages:
|
|||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
|
||||
fi
|
||||
displayName: Enable Extra Platform Support
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
|
|
@ -122,27 +122,23 @@ stages:
|
|||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
- publish: '$(testsFolder)/net8.0/win-x64/publish'
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
- publish: '$(testsFolder)/net8.0/linux-x64/publish'
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
- publish: '$(testsFolder)/net8.0/linux-musl-x64/publish'
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
- publish: '$(testsFolder)/net8.0/freebsd-x64/publish'
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
- publish: '$(testsFolder)/net8.0/osx-x64/publish'
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
|
@ -260,21 +256,21 @@ stages:
|
|||
archiveFile: '$(Build.ArtifactStagingDirectory)/Lidarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create win-x86 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Lidarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-x64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Lidarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-x64 tar
|
||||
inputs:
|
||||
|
|
@ -282,14 +278,14 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-arm64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Lidarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-arm64 tar
|
||||
inputs:
|
||||
|
|
@ -297,7 +293,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-x64 tar
|
||||
inputs:
|
||||
|
|
@ -305,7 +301,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-musl-x64 tar
|
||||
inputs:
|
||||
|
|
@ -313,15 +309,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-x86 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Lidarr.$(buildName).linux-core-x86.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm tar
|
||||
inputs:
|
||||
|
|
@ -329,7 +317,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-musl-arm tar
|
||||
inputs:
|
||||
|
|
@ -337,7 +325,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm64 tar
|
||||
inputs:
|
||||
|
|
@ -345,7 +333,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
inputs:
|
||||
|
|
@ -353,7 +341,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net8.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
|
|
@ -361,7 +349,7 @@ stages:
|
|||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net8.0
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
|
|
@ -493,10 +481,6 @@ stages:
|
|||
testName: 'Musl Net Core'
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
|
@ -510,12 +494,6 @@ stages:
|
|||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
|
|
@ -922,11 +900,6 @@ stages:
|
|||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
pattern: 'Lidarr.*.linux-musl-core-x64.tar.gz'
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
pattern: 'Lidarr.*.linux-core-x86.tar.gz'
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
|
|
@ -939,12 +912,6 @@ stages:
|
|||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
|
|
@ -1220,22 +1187,23 @@ stages:
|
|||
extraProperties: |
|
||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
||||
sonar.coverage.exclusions=**/Lidarr.Api.V1/**/*
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.cobertura.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.cobertura.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
./build.sh --backend -f net8.0 -r win-x64
|
||||
TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5.3.11
|
||||
- task: reportgenerator@5
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.cobertura.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
publishCodeCoverageResults: true
|
||||
sourcedirs: src
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
|
|
@ -1266,4 +1234,3 @@ stages:
|
|||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
|
|
|
|||
52
build.sh
52
build.sh
|
|
@ -33,14 +33,14 @@ EnableExtraPlatformsInSDK()
|
|||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +79,9 @@ Build()
|
|||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
||||
dotnet msbuild -restore $slnFile -p:SelfContained=true -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
||||
else
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
||||
dotnet msbuild -restore $slnFile -p:SelfContained=true -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
||||
fi
|
||||
|
||||
ProgressEnd 'Build'
|
||||
|
|
@ -137,7 +137,7 @@ PackageLinux()
|
|||
|
||||
echo "Adding Lidarr.Mono to UpdatePackage"
|
||||
cp $folder/Lidarr.Mono.* $folder/Lidarr.Update
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
if [ "$framework" = "net8.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Lidarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Lidarr.Update
|
||||
fi
|
||||
|
|
@ -165,7 +165,7 @@ PackageMacOS()
|
|||
|
||||
echo "Adding Lidarr.Mono to UpdatePackage"
|
||||
cp $folder/Lidarr.Mono.* $folder/Lidarr.Update
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
if [ "$framework" = "net8.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Lidarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Lidarr.Update
|
||||
fi
|
||||
|
|
@ -377,15 +377,14 @@ then
|
|||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
PackageTests "net8.0" "win-x64"
|
||||
PackageTests "net8.0" "win-x86"
|
||||
PackageTests "net8.0" "linux-x64"
|
||||
PackageTests "net8.0" "linux-musl-x64"
|
||||
PackageTests "net8.0" "osx-x64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
PackageTests "net8.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
|
|
@ -413,20 +412,19 @@ then
|
|||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
Package "net8.0" "win-x64"
|
||||
Package "net8.0" "win-x86"
|
||||
Package "net8.0" "linux-x64"
|
||||
Package "net8.0" "linux-musl-x64"
|
||||
Package "net8.0" "linux-arm64"
|
||||
Package "net8.0" "linux-musl-arm64"
|
||||
Package "net8.0" "linux-arm"
|
||||
Package "net8.0" "linux-musl-arm"
|
||||
Package "net8.0" "osx-x64"
|
||||
Package "net8.0" "osx-arm64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
Package "net8.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
|
|
@ -436,7 +434,7 @@ fi
|
|||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
BuildInstaller "net8.0" "win-x64"
|
||||
BuildInstaller "net8.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
|
|
|||
4
docs.sh
4
docs.sh
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
FRAMEWORK="net8.0"
|
||||
PLATFORM=$1
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ dotnet clean $slnFile -c Release
|
|||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 9.0.6 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
|
||||
|
|
|
|||
27
frontend/src/Artist/ArtistStatus.js
Normal file
27
frontend/src/Artist/ArtistStatus.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export function getArtistStatusDetails(status, artistType) {
|
||||
|
||||
let statusDetails = {
|
||||
icon: icons.ARTIST_CONTINUING,
|
||||
title: translate('Continuing'),
|
||||
message: translate('ContinuingMoreAlbumsAreExpected')
|
||||
};
|
||||
|
||||
if (status === 'deleted') {
|
||||
statusDetails = {
|
||||
icon: icons.ARTIST_DELETED,
|
||||
title: translate('Deleted'),
|
||||
message: translate('ArtistWasDeletedFromMusicBrainz')
|
||||
};
|
||||
} else if (status === 'ended') {
|
||||
statusDetails = {
|
||||
icon: icons.ARTIST_ENDED,
|
||||
title: artistType === 'Person' ? translate('Deceased') : translate('Inactive'),
|
||||
message: translate('ContinuingNoAdditionalAlbumsAreExpected')
|
||||
};
|
||||
}
|
||||
|
||||
return statusDetails;
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import translate from 'Utilities/String/translate';
|
|||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
import { getArtistStatusDetails } from '../ArtistStatus';
|
||||
import ArtistAlternateTitles from './ArtistAlternateTitles';
|
||||
import ArtistDetailsLinks from './ArtistDetailsLinks';
|
||||
import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
|
||||
|
|
@ -196,7 +197,6 @@ class ArtistDetails extends Component {
|
|||
overview,
|
||||
links,
|
||||
images,
|
||||
artistType,
|
||||
alternateTitles,
|
||||
tags,
|
||||
isSaving,
|
||||
|
|
@ -213,7 +213,8 @@ class ArtistDetails extends Component {
|
|||
nextArtist,
|
||||
onMonitorTogglePress,
|
||||
onRefreshPress,
|
||||
onSearchPress
|
||||
onSearchPress,
|
||||
artistType
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
|
@ -236,8 +237,7 @@ class ArtistDetails extends Component {
|
|||
expandedState
|
||||
} = this.state;
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
const endedString = artistType === 'Person' ? translate('Deceased') : translate('Inactive');
|
||||
const statusDetails = getArtistStatusDetails(status, artistType);
|
||||
|
||||
let trackFilesCountMessage = translate('TrackFilesCountMessage');
|
||||
|
||||
|
|
@ -526,16 +526,16 @@ class ArtistDetails extends Component {
|
|||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={continuing ? translate('ContinuingMoreAlbumsAreExpected') : translate('ContinuingNoAdditionalAlbumsAreExpected')}
|
||||
title={statusDetails.message}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={continuing ? icons.ARTIST_CONTINUING : icons.ARTIST_ENDED}
|
||||
name={statusDetails.icon}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{continuing ? translate('Continuing') : endedString}
|
||||
{statusDetails.title}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,6 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
|
|||
key={name}
|
||||
className={styles[name]}
|
||||
artistId={artistId}
|
||||
artistType={artistType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
isSelectMode={isSelectMode}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getArtistStatusDetails } from 'Artist/ArtistStatus';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
|
|
@ -11,9 +12,9 @@ import styles from './ArtistStatusCell.css';
|
|||
interface ArtistStatusCellProps {
|
||||
className: string;
|
||||
artistId: number;
|
||||
artistType?: string;
|
||||
monitored: boolean;
|
||||
status: string;
|
||||
artistType?: string;
|
||||
isSelectMode: boolean;
|
||||
isSaving: boolean;
|
||||
component?: React.ElementType;
|
||||
|
|
@ -23,17 +24,16 @@ function ArtistStatusCell(props: ArtistStatusCellProps) {
|
|||
const {
|
||||
className,
|
||||
artistId,
|
||||
artistType,
|
||||
monitored,
|
||||
status,
|
||||
artistType,
|
||||
isSelectMode,
|
||||
isSaving,
|
||||
component: Component = VirtualTableRowCell,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const endedString =
|
||||
artistType === 'Person' ? translate('Deceased') : translate('Inactive');
|
||||
const statusDetails = getArtistStatusDetails(status, artistType);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onMonitoredPress = useCallback(() => {
|
||||
|
|
@ -63,10 +63,8 @@ function ArtistStatusCell(props: ArtistStatusCellProps) {
|
|||
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
||||
title={
|
||||
status === 'ended' ? endedString : translate('StatusEndedContinuing')
|
||||
}
|
||||
name={statusDetails.icon}
|
||||
title={`${statusDetails.title}: ${statusDetails.message}`}
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
const artistStatusList = [
|
||||
{ id: 'continuing', name: 'Continuing' },
|
||||
{ id: 'ended', name: 'Inactive' }
|
||||
];
|
||||
|
|
@ -9,7 +9,7 @@ const protocols = [
|
|||
function ArtistStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
tagList={artistStatusList}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
.modal {
|
||||
position: relative;
|
||||
display: flex;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
border-radius: 6px;
|
||||
opacity: 1;
|
||||
|
|
|
|||
56
frontend/src/Helpers/Hooks/useTheme.ts
Normal file
56
frontend/src/Helpers/Hooks/useTheme.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(theme) => theme
|
||||
);
|
||||
}
|
||||
|
||||
const useTheme = () => {
|
||||
const selectedTheme = useSelector(createThemeSelector());
|
||||
const [resolvedTheme, setResolvedTheme] = useState(selectedTheme);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTheme !== 'auto') {
|
||||
setResolvedTheme(selectedTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
const applySystemTheme = () => {
|
||||
setResolvedTheme(
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
);
|
||||
};
|
||||
|
||||
applySystemTheme();
|
||||
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', applySystemTheme);
|
||||
|
||||
return () => {
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.removeEventListener('change', applySystemTheme);
|
||||
};
|
||||
}, [selectedTheme]);
|
||||
|
||||
return resolvedTheme;
|
||||
};
|
||||
|
||||
export default useTheme;
|
||||
|
||||
export const useThemeColor = (color: string) => {
|
||||
const theme = useTheme();
|
||||
const themeVariables = themes[theme];
|
||||
|
||||
// @ts-expect-error - themeVariables is a string indexable type
|
||||
return themeVariables[color];
|
||||
};
|
||||
|
|
@ -209,6 +209,7 @@ export const SCORE = fasUserPlus;
|
|||
export const SEARCH = fasSearch;
|
||||
export const ARTIST_CONTINUING = fasPlay;
|
||||
export const ARTIST_ENDED = fasStop;
|
||||
export const ARTIST_DELETED = fasExclamationTriangle;
|
||||
export const SETTINGS = fasCogs;
|
||||
export const SHUTDOWN = fasPowerOff;
|
||||
export const SORT = fasSort;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.path {
|
||||
margin-right: 20px;
|
||||
color: var(--dimColor);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.path {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
12
frontend/src/InteractiveImport/Track/SelectTrackModalContent.css.d.ts
vendored
Normal file
12
frontend/src/InteractiveImport/Track/SelectTrackModalContent.css.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttons': string;
|
||||
'filterInput': string;
|
||||
'footer': string;
|
||||
'modalBody': string;
|
||||
'path': string;
|
||||
'scroller': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import ExpandingFileDetails from 'TrackFile/ExpandingFileDetails';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
|
@ -17,6 +19,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import SelectTrackRow from './SelectTrackRow';
|
||||
import styles from './SelectTrackModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -65,11 +68,12 @@ class SelectTrackModalContent extends Component {
|
|||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
filter: '',
|
||||
lastToggled: null,
|
||||
selectedState: init
|
||||
};
|
||||
|
||||
props.onSortPress( props.sortKey, props.sortDirection );
|
||||
props.onSortPress(props.sortKey, props.sortDirection);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -82,6 +86,10 @@ class SelectTrackModalContent extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
|
@ -119,8 +127,10 @@ class SelectTrackModalContent extends Component {
|
|||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
filter,
|
||||
selectedState
|
||||
} = this.state;
|
||||
const filterTrackNumber = parseInt(filter);
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load tracks');
|
||||
|
||||
|
|
@ -141,15 +151,29 @@ class SelectTrackModalContent extends Component {
|
|||
{translate('ManualImport')} - {translate('SelectTracks')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder={translate('FilterTracksByTitleOrNumber')}
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
|
||||
{
|
||||
error &&
|
||||
<div>{errorMessage}</div>
|
||||
error ? <div>{errorMessage}</div> : null
|
||||
}
|
||||
|
||||
<ExpandingFileDetails
|
||||
|
|
@ -160,7 +184,7 @@ class SelectTrackModalContent extends Component {
|
|||
/>
|
||||
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
isPopulated && !!items.length ?
|
||||
<Table
|
||||
columns={selectAllEnabled ? columns : selectAllBlankColumn.concat(columns)}
|
||||
selectAll={selectAllEnabled}
|
||||
|
|
@ -174,7 +198,10 @@ class SelectTrackModalContent extends Component {
|
|||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
return item.title.toLowerCase().includes(filter) ||
|
||||
item.absoluteTrackNumber === filterTrackNumber ||
|
||||
item.trackNumber === filterTrackNumber ?
|
||||
(
|
||||
<SelectTrackRow
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
|
|
@ -187,17 +214,21 @@ class SelectTrackModalContent extends Component {
|
|||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
) :
|
||||
null;
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length &&
|
||||
'No tracks were found for the selected album'
|
||||
isPopulated && !items.length ?
|
||||
translate('NoTracksFoundForSelectedAlbum') :
|
||||
null
|
||||
}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ export const authenticationMethodOptions = [
|
|||
key: 'basic',
|
||||
get value() {
|
||||
return translate('AuthBasic');
|
||||
}
|
||||
},
|
||||
isDisabled: true,
|
||||
isHidden: true
|
||||
},
|
||||
{
|
||||
key: 'forms',
|
||||
|
|
|
|||
5
global.json
Normal file
5
global.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "8.0.405"
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@microsoft/signalr": "8.0.17",
|
||||
"@sentry/browser": "7.119.1",
|
||||
"@sentry/integrations": "7.119.1",
|
||||
"@types/node": "20.16.11",
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
"html-webpack-plugin": "5.6.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"postcss": "8.4.47",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
|
||||
<Deterministic Condition="$(AssemblyVersion.EndsWith('*'))">False</Deterministic>
|
||||
|
||||
<PathMap>$(MSBuildProjectDirectory)=./$(MSBuildProjectName)/</PathMap>
|
||||
<PathMap>$(MSBuildThisFileDirectory)=./</PathMap>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Set the AssemblyConfiguration attribute for projects -->
|
||||
|
|
@ -99,13 +99,6 @@
|
|||
<RootNamespace Condition="'$(LidarrProject)'=='true'">$(MSBuildProjectName.Replace('Lidarr','NzbDrone'))</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TestProject)'!='true'">
|
||||
<!-- Annotates .NET assemblies with repository information including SHA -->
|
||||
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
||||
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Sentry specific configuration: Only in Release mode -->
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||
|
|
@ -130,14 +123,11 @@
|
|||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.1.20" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(LidarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ public List<AlbumResource> GetAlbums([FromQuery]int? artistId,
|
|||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<AlbumResource> AddAlbum(AlbumResource albumResource)
|
||||
public ActionResult<AlbumResource> AddAlbum([FromBody] AlbumResource albumResource)
|
||||
{
|
||||
var album = _addAlbumService.AddAlbum(albumResource.ToModel());
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ public ActionResult<AlbumResource> AddAlbum(AlbumResource albumResource)
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<AlbumResource> UpdateAlbum(AlbumResource albumResource)
|
||||
public ActionResult<AlbumResource> UpdateAlbum([FromBody] AlbumResource albumResource)
|
||||
{
|
||||
var album = _albumService.GetAlbum(albumResource.Id);
|
||||
|
||||
|
|
@ -171,7 +171,7 @@ public void DeleteAlbum(int id, bool deleteFiles = false, bool addImportListExcl
|
|||
}
|
||||
|
||||
[HttpPut("monitor")]
|
||||
public IActionResult SetAlbumsMonitored([FromBody]AlbumsMonitoredResource resource)
|
||||
public IActionResult SetAlbumsMonitored([FromBody] AlbumsMonitoredResource resource)
|
||||
{
|
||||
_albumService.SetMonitored(resource.AlbumIds, resource.Monitored);
|
||||
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ public List<ArtistResource> AllArtists(Guid? mbId)
|
|||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<ArtistResource> AddArtist(ArtistResource artistResource)
|
||||
public ActionResult<ArtistResource> AddArtist([FromBody] ArtistResource artistResource)
|
||||
{
|
||||
var artist = _addArtistService.AddArtist(artistResource.ToModel());
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ public ActionResult<ArtistResource> AddArtist(ArtistResource artistResource)
|
|||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<ArtistResource> UpdateArtist(ArtistResource artistResource, bool moveFiles = false)
|
||||
public ActionResult<ArtistResource> UpdateArtist([FromBody] ArtistResource artistResource, bool moveFiles = false)
|
||||
{
|
||||
var artist = _artistService.GetArtist(artistResource.Id);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public override AutoTaggingResource GetResourceById(int id)
|
|||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<AutoTaggingResource> Create(AutoTaggingResource autoTagResource)
|
||||
public ActionResult<AutoTaggingResource> Create([FromBody] AutoTaggingResource autoTagResource)
|
||||
{
|
||||
var model = autoTagResource.ToModel(_specifications);
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ public ActionResult<AutoTaggingResource> Create(AutoTaggingResource autoTagResou
|
|||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<AutoTaggingResource> Update(AutoTaggingResource resource)
|
||||
public ActionResult<AutoTaggingResource> Update([FromBody] AutoTaggingResource resource)
|
||||
{
|
||||
var model = resource.ToModel(_specifications);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public override CommandResource GetResourceById(int id)
|
|||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<CommandResource> StartCommand(CommandResource commandResource)
|
||||
public ActionResult<CommandResource> StartCommand([FromBody] CommandResource commandResource)
|
||||
{
|
||||
var commandType =
|
||||
_knownTypes.GetImplementations(typeof(Command))
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public TResource GetConfig()
|
|||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public virtual ActionResult<TResource> SaveConfig(TResource resource)
|
||||
public virtual ActionResult<TResource> SaveConfig([FromBody] TResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
|
|
|
|||
|
|
@ -40,10 +40,14 @@ public HostConfigController(IConfigFileProvider configFileProvider,
|
|||
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||
SharedValidator.RuleFor(c => c.InstanceName).ContainsLidarr().When(c => c.InstanceName.IsNotNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Basic ||
|
||||
c.AuthenticationMethod == AuthenticationType.Forms);
|
||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Basic ||
|
||||
c.AuthenticationMethod == AuthenticationType.Forms);
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms);
|
||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms);
|
||||
|
||||
SharedValidator.RuleFor(c => c.AuthenticationMethod)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.NotEqual(AuthenticationType.Basic)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
.WithMessage("'Basic' is no longer supported, switch to 'Forms' instead.");
|
||||
|
||||
SharedValidator.RuleFor(c => c.PasswordConfirmation)
|
||||
.Must((resource, p) => IsMatchingPassword(resource)).WithMessage("Must match Password");
|
||||
|
|
@ -107,7 +111,7 @@ public HostConfigResource GetHostConfig()
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource)
|
||||
public ActionResult<HostConfigResource> SaveHostConfig([FromBody] HostConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public NamingConfigResource GetNamingConfig()
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<NamingConfigResource> UpdateNamingConfig(NamingConfigResource resource)
|
||||
public ActionResult<NamingConfigResource> UpdateNamingConfig([FromBody] NamingConfigResource resource)
|
||||
{
|
||||
var nameSpec = resource.ToModel();
|
||||
ValidateFormatResult(nameSpec);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public UiConfigController(IConfigFileProvider configFileProvider, IConfigService
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource)
|
||||
public override ActionResult<UiConfigResource> SaveConfig([FromBody] UiConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public List<CustomFilterResource> GetCustomFilters()
|
|||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource)
|
||||
public ActionResult<CustomFilterResource> AddCustomFilter([FromBody] CustomFilterResource resource)
|
||||
{
|
||||
var customFilter = _customFilterService.Add(resource.ToModel());
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource r
|
|||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource)
|
||||
public ActionResult<CustomFilterResource> UpdateCustomFilter([FromBody] CustomFilterResource resource)
|
||||
{
|
||||
_customFilterService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public List<CustomFormatResource> GetAll()
|
|||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource)
|
||||
public ActionResult<CustomFormatResource> Create([FromBody] CustomFormatResource customFormatResource)
|
||||
{
|
||||
var model = customFormatResource.ToModel(_specifications);
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ public ActionResult<CustomFormatResource> Create(CustomFormatResource customForm
|
|||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFormatResource> Update(CustomFormatResource resource)
|
||||
public ActionResult<CustomFormatResource> Update([FromBody] CustomFormatResource resource)
|
||||
{
|
||||
var model = resource.ToModel(_specifications);
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public List<ImportListExclusionResource> GetImportListExclusions()
|
|||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportListExclusionResource resource)
|
||||
public ActionResult<ImportListExclusionResource> AddImportListExclusion([FromBody] ImportListExclusionResource resource)
|
||||
{
|
||||
var customFilter = _importListExclusionService.Add(resource.ToModel());
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportLi
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion(ImportListExclusionResource resource)
|
||||
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion([FromBody] ImportListExclusionResource resource)
|
||||
{
|
||||
_importListExclusionService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public ReleaseController(IAlbumService albumService,
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ReleaseResource>> DownloadRelease(ReleaseResource release)
|
||||
public async Task<ActionResult<ReleaseResource>> DownloadRelease([FromBody] ReleaseResource release)
|
||||
{
|
||||
ValidateResource(release);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker,
|
|||
|
||||
[HttpPost]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<ReleaseResource> Create(ReleaseResource release)
|
||||
public ActionResult<ReleaseResource> Create([FromBody] ReleaseResource release)
|
||||
{
|
||||
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lidarr.Http\Lidarr.Http.csproj" />
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<ProjectReference Include="..\NzbDrone.SignalR\Lidarr.SignalR.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.6" />
|
||||
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public ManualImportController(IManualImportService manualImportService,
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult UpdateItems(List<ManualImportUpdateResource> resource)
|
||||
public IActionResult UpdateItems([FromBody] List<ManualImportUpdateResource> resource)
|
||||
{
|
||||
return Accepted(UpdateImportItems(resource));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public DelayProfileController(IDelayProfileService delayProfileService, DelayPro
|
|||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<DelayProfileResource> Create(DelayProfileResource resource)
|
||||
public ActionResult<DelayProfileResource> Create([FromBody] DelayProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _delayProfileService.Add(model);
|
||||
|
|
@ -54,7 +54,7 @@ public void DeleteProfile(int id)
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<DelayProfileResource> Update(DelayProfileResource resource)
|
||||
public ActionResult<DelayProfileResource> Update([FromBody] DelayProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
_delayProfileService.Update(model);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public MetadataProfileController(IMetadataProfileService profileService)
|
|||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<MetadataProfileResource> Create(MetadataProfileResource resource)
|
||||
public ActionResult<MetadataProfileResource> Create([FromBody] MetadataProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
|
|
@ -38,7 +38,7 @@ public void DeleteProfile(int id)
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<MetadataProfileResource> Update(MetadataProfileResource resource)
|
||||
public ActionResult<MetadataProfileResource> Update([FromBody] MetadataProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public QualityProfileController(IQualityProfileService qualityProfileService, IC
|
|||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<QualityProfileResource> Create(QualityProfileResource resource)
|
||||
public ActionResult<QualityProfileResource> Create([FromBody] QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _qualityProfileService.Add(model);
|
||||
|
|
@ -58,7 +58,7 @@ public void DeleteProfile(int id)
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<QualityProfileResource> Update(QualityProfileResource resource)
|
||||
public ActionResult<QualityProfileResource> Update([FromBody] QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ public List<ReleaseProfileResource> GetAll()
|
|||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource)
|
||||
public ActionResult<ReleaseProfileResource> Create([FromBody] ReleaseProfileResource resource)
|
||||
{
|
||||
return Created(_releaseProfileService.Add(resource.ToModel()).Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource)
|
||||
public ActionResult<ReleaseProfileResource> Update([FromBody] ReleaseProfileResource resource)
|
||||
{
|
||||
_releaseProfileService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public QualityDefinitionController(IQualityDefinitionService qualityDefinitionSe
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<QualityDefinitionResource> Update(QualityDefinitionResource resource)
|
||||
public ActionResult<QualityDefinitionResource> Update([FromBody] QualityDefinitionResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
_qualityDefinitionService.Update(model);
|
||||
|
|
|
|||
|
|
@ -147,8 +147,8 @@ public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource
|
|||
var filteredQueue = includeUnknownArtistItems ? queue : queue.Where(q => q.Artist != null);
|
||||
var pending = _pendingReleaseService.GetPendingQueue();
|
||||
|
||||
var hasArtistIdFilter = artistIds.Any();
|
||||
var hasQualityFilter = quality.Any();
|
||||
var hasArtistIdFilter = artistIds is { Count: > 0 };
|
||||
var hasQualityFilter = quality is { Count: > 0 };
|
||||
|
||||
var fullQueue = filteredQueue.Concat(pending).Where(q =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public override RootFolderResource GetResourceById(int id)
|
|||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<RootFolderResource> CreateRootFolder(RootFolderResource rootFolderResource)
|
||||
public ActionResult<RootFolderResource> CreateRootFolder([FromBody] RootFolderResource rootFolderResource)
|
||||
{
|
||||
var model = rootFolderResource.ToModel();
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ public ActionResult<RootFolderResource> CreateRootFolder(RootFolderResource root
|
|||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<RootFolderResource> UpdateRootFolder(RootFolderResource rootFolderResource)
|
||||
public ActionResult<RootFolderResource> UpdateRootFolder([FromBody] RootFolderResource rootFolderResource)
|
||||
{
|
||||
var model = rootFolderResource.ToModel();
|
||||
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@ public List<TagResource> GetAll()
|
|||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<TagResource> Create(TagResource resource)
|
||||
public ActionResult<TagResource> Create([FromBody] TagResource resource)
|
||||
{
|
||||
return Created(_tagService.Add(resource.ToModel()).Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<TagResource> Update(TagResource resource)
|
||||
public ActionResult<TagResource> Update([FromBody] TagResource resource)
|
||||
{
|
||||
_tagService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"openapi": "3.0.1",
|
||||
"openapi": "3.0.4",
|
||||
"info": {
|
||||
"title": "Lidarr",
|
||||
"description": "Lidarr API docs",
|
||||
|
|
|
|||
|
|
@ -27,9 +27,8 @@ public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthentic
|
|||
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
IConfigFileProvider config)
|
||||
: base(options, logger, encoder, clock)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
_apiKey = config.ApiKey;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authent
|
|||
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(name, options);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddBasic(this AuthenticationBuilder authenticationBuilder, string name)
|
||||
{
|
||||
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(name, options => { });
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddNone(this AuthenticationBuilder authenticationBuilder, string name)
|
||||
{
|
||||
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
|
||||
|
|
@ -35,7 +30,7 @@ public static AuthenticationBuilder AddExternal(this AuthenticationBuilder authe
|
|||
|
||||
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<CookieAuthenticationOptions>(AuthenticationType.Forms.ToString())
|
||||
services.AddOptions<CookieAuthenticationOptions>(nameof(AuthenticationType.Forms))
|
||||
.Configure<IConfigFileProvider>((options, configFileProvider) =>
|
||||
{
|
||||
// Replace diacritics and replace non-word characters to ensure cookie name doesn't contain any valid URL characters not allowed in cookie names
|
||||
|
|
@ -52,10 +47,9 @@ public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection
|
|||
});
|
||||
|
||||
return services.AddAuthentication()
|
||||
.AddNone(AuthenticationType.None.ToString())
|
||||
.AddExternal(AuthenticationType.External.ToString())
|
||||
.AddBasic(AuthenticationType.Basic.ToString())
|
||||
.AddCookie(AuthenticationType.Forms.ToString())
|
||||
.AddNone(nameof(AuthenticationType.None))
|
||||
.AddExternal(nameof(AuthenticationType.External))
|
||||
.AddCookie(nameof(AuthenticationType.Forms))
|
||||
.AddApiKey("API", options =>
|
||||
{
|
||||
options.HeaderName = "X-Api-Key";
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Authentication;
|
||||
|
||||
namespace Lidarr.Http.Authentication
|
||||
{
|
||||
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
|
||||
public BasicAuthenticationHandler(IAuthenticationService authService,
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.ContainsKey("Authorization"))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
|
||||
}
|
||||
|
||||
// Get authorization key
|
||||
var authorizationHeader = Request.Headers["Authorization"].ToString();
|
||||
var authHeaderRegex = new Regex(@"Basic (.*)");
|
||||
|
||||
if (!authHeaderRegex.IsMatch(authorizationHeader))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly."));
|
||||
}
|
||||
|
||||
var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1")));
|
||||
var authSplit = authBase64.Split(':', 2);
|
||||
var authUsername = authSplit[0];
|
||||
var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password");
|
||||
|
||||
var user = _authService.Login(Request, authUsername, authPassword);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct."));
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim("user", user.Username),
|
||||
new Claim("identifier", user.Identifier.ToString()),
|
||||
new Claim("AuthType", AuthenticationType.Basic.ToString())
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, "Basic", "user", "identifier");
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, "Basic");
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.Headers.Add("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\"");
|
||||
Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.StatusCode = 403;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,8 @@ public class NoAuthenticationHandler : AuthenticationHandler<AuthenticationSchem
|
|||
{
|
||||
public NoAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Http.Authentication
|
|||
{
|
||||
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
||||
{
|
||||
private const string POLICY_NAME = "UI";
|
||||
private const string PolicyName = "UI";
|
||||
private readonly IConfigFileProvider _config;
|
||||
|
||||
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
|
||||
|
|
@ -26,7 +26,7 @@ public UiAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options,
|
|||
|
||||
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
|
||||
{
|
||||
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase))
|
||||
if (policyName.Equals(PolicyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
|
||||
.AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement());
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="ImpromptuInterface" Version="7.0.1" />
|
||||
<PackageReference Include="ImpromptuInterface" Version="8.0.6" />
|
||||
<PackageReference Include="NLog" Version="5.4.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public async Task InvokeAsync(HttpContext context)
|
|||
{
|
||||
if (context.Request.IsApiRequest() && !context.Response.Headers.ContainsKey(VERSIONHEADER))
|
||||
{
|
||||
context.Response.Headers.Add(VERSIONHEADER, _version);
|
||||
context.Response.Headers[VERSIONHEADER] = _version;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,5 @@
|
|||
<add key="Taglib" value="https://pkgs.dev.azure.com/Lidarr/Lidarr/_packaging/Taglib/nuget/v3/index.json" />
|
||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public class BuildInfoFixture
|
|||
[Test]
|
||||
public void should_return_version()
|
||||
{
|
||||
BuildInfo.Version.Major.Should().BeOneOf(2, 10);
|
||||
BuildInfo.Version.Major.Should().BeOneOf(3, 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Host\Lidarr.Host.csproj" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
|
|
@ -24,10 +23,5 @@ public DestinationAlreadyExistsException(string message, Exception innerExceptio
|
|||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected DestinationAlreadyExistsException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class AppFolderFactory : IAppFolderFactory
|
|||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IDiskTransferService _diskTransferService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AppFolderFactory(IAppFolderInfo appFolderInfo,
|
||||
|
|
@ -27,6 +28,7 @@ public AppFolderFactory(IAppFolderInfo appFolderInfo,
|
|||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
_diskTransferService = diskTransferService;
|
||||
_logger = NzbDroneLogger.GetLogger(this);
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +36,7 @@ public void Register()
|
|||
{
|
||||
try
|
||||
{
|
||||
MigrateAppDataFolder();
|
||||
_diskProvider.EnsureFolder(_appFolderInfo.AppDataFolder);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
|
|
@ -66,6 +69,28 @@ public void SetPermissions()
|
|||
}
|
||||
}
|
||||
|
||||
private void MigrateAppDataFolder()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OsInfo.IsOsx)
|
||||
{
|
||||
var userAppDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), ".config", "Lidarr");
|
||||
|
||||
if (_diskProvider.FolderExists(userAppDataFolder) && !_diskProvider.FileExists(_appFolderInfo.GetConfigPath()))
|
||||
{
|
||||
_diskTransferService.MirrorFolder(userAppDataFolder, _appFolderInfo.AppDataFolder);
|
||||
_diskProvider.DeleteFolder(userAppDataFolder, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex, ex.Message);
|
||||
throw new LidarrStartupException(ex, "Unable to migrate configuration folder to {0}. Migrate manually", _appFolderInfo.AppDataFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeMonoApplicationData()
|
||||
{
|
||||
if (OsInfo.IsWindows)
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ public AppFolderInfo(IStartupContext startupContext)
|
|||
_dataSpecialFolder = Environment.SpecialFolder.ApplicationData;
|
||||
}
|
||||
|
||||
if (startupContext.Args.ContainsKey(StartupContext.APPDATA))
|
||||
if (startupContext.Args.TryGetValue(StartupContext.APPDATA, out var argsAppDataFolder))
|
||||
{
|
||||
AppDataFolder = startupContext.Args[StartupContext.APPDATA];
|
||||
AppDataFolder = argsAppDataFolder;
|
||||
Logger.Info("Data directory is being overridden to [{0}]", AppDataFolder);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
|
|||
IsWindowsService = hostLifetime is WindowsServiceLifetime;
|
||||
IsStarting = true;
|
||||
|
||||
// net6.0 will return Lidarr.dll for entry assembly, we need the actual
|
||||
// net8.0 will return Lidarr.dll for entry assembly, we need the actual
|
||||
// executable name (Lidarr on linux). On mono this will return the location of
|
||||
// the mono executable itself, which is not what we want.
|
||||
var entry = Process.GetCurrentProcess().MainModule;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="IPAddressRange" Version="6.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="NLog" Version="5.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.3" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.4" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.2" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="2.0.2" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.6" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.6.1" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="EnsureThat\Resources\ExceptionMessages.Designer.cs">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
|
||||
<ApplicationIcon>..\NzbDrone.Host\NzbDrone.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
namespace NzbDrone.Core.Test.Http
|
||||
{
|
||||
[TestFixture]
|
||||
[Platform(Exclude = "MacOsX")]
|
||||
public class HttpProxySettingsProviderFixture : TestBase<HttpProxySettingsProvider>
|
||||
{
|
||||
private HttpProxySettings GetProxySettings()
|
||||
|
|
@ -15,24 +16,24 @@ private HttpProxySettings GetProxySettings()
|
|||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_bypass_proxy()
|
||||
[TestCase("http://eu.httpbin.org/get")]
|
||||
[TestCase("http://google.com/get")]
|
||||
[TestCase("http://localhost:8654/get")]
|
||||
[TestCase("http://172.21.0.1:8989/api/v3/indexer/schema")]
|
||||
public void should_bypass_proxy(string url)
|
||||
{
|
||||
var settings = GetProxySettings();
|
||||
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri(url)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_bypass_proxy()
|
||||
[TestCase("http://bing.com/get")]
|
||||
[TestCase("http://172.3.0.1:8989/api/v3/indexer/schema")]
|
||||
public void should_not_bypass_proxy(string url)
|
||||
{
|
||||
var settings = GetProxySettings();
|
||||
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri(url)).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ public void map_artist_should_not_update_id_if_http_throws()
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pending mapping fixes", Until = "2025-10-20 00:00:00Z")]
|
||||
[Ignore("Pending mapping fixes", Until = "2025-12-31 00:00:00Z")]
|
||||
public void map_artist_should_work()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
|
@ -159,7 +159,7 @@ public void map_album_should_not_update_id_if_http_throws()
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pending mapping fixes", Until = "2025-10-20 00:00:00Z")]
|
||||
[Ignore("Pending mapping fixes", Until = "2025-12-31 00:00:00Z")]
|
||||
public void map_album_should_work()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
namespace NzbDrone.Core.Test.MetadataSource.SkyHook
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-10-01 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-12-31 00:00:00Z")]
|
||||
public class SkyHookProxyFixture : CoreTest<SkyHookProxy>
|
||||
{
|
||||
private MetadataProfile _metadataProfile;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
namespace NzbDrone.Core.Test.MetadataSource.SkyHook
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-10-01 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-12-31 00:00:00Z")]
|
||||
public class SkyHookProxySearchFixture : CoreTest<SkyHookProxy>
|
||||
{
|
||||
[SetUp]
|
||||
|
|
|
|||
|
|
@ -135,8 +135,8 @@ public void should_not_find_artist_if_multiple_artists_have_same_name()
|
|||
|
||||
_artistRepo.All().Should().HaveCount(4);
|
||||
|
||||
var artist = _artistRepo.FindByName(Parser.Parser.CleanArtistName(name));
|
||||
artist.Should().BeNull();
|
||||
Action act = () => _artistRepo.FindByName(Parser.Parser.CleanArtistName(name));
|
||||
act.Should().Throw<MultipleArtistsFoundException>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ public void Setup()
|
|||
_remoteAlbums = _albums.JsonClone();
|
||||
_remoteAlbums.ForEach(x => x.Id = 0);
|
||||
|
||||
var metadata = Builder<ArtistMetadata>.CreateNew().Build();
|
||||
var metadata = Builder<ArtistMetadata>.CreateNew()
|
||||
.With(m => m.Status = ArtistStatusType.Continuing)
|
||||
.Build();
|
||||
|
||||
_artist = Builder<Artist>.CreateNew()
|
||||
.With(a => a.Metadata = metadata)
|
||||
|
|
@ -53,6 +55,14 @@ public void Setup()
|
|||
.Setup(s => s.GetArtists(new List<int> { _artist.Id }))
|
||||
.Returns(new List<Artist> { _artist });
|
||||
|
||||
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
|
||||
.Setup(s => s.GetArtist(It.IsAny<int>()))
|
||||
.Returns(_artist);
|
||||
|
||||
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
|
||||
.Setup(s => s.FindById(It.IsAny<string>()))
|
||||
.Returns(default(Artist));
|
||||
|
||||
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
|
||||
.Setup(s => s.InsertMany(It.IsAny<List<Album>>()));
|
||||
|
||||
|
|
@ -109,8 +119,12 @@ private void GivenAlbumsForRefresh(List<Album> albums)
|
|||
private void AllowArtistUpdate()
|
||||
{
|
||||
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
|
||||
.Setup(x => x.UpdateArtist(It.IsAny<Artist>(), It.IsAny<bool>()))
|
||||
.Returns((Artist a, bool updated) => a);
|
||||
.Setup(x => x.UpdateArtist(It.IsAny<Artist>(), true))
|
||||
.Returns((Artist a, bool publishEvent) => a);
|
||||
|
||||
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
|
||||
.Setup(x => x.UpdateArtist(It.IsAny<Artist>(), false))
|
||||
.Returns((Artist a, bool publishEvent) => a);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -177,13 +191,16 @@ public void should_call_new_album_monitor_service_when_adding_album()
|
|||
[Test]
|
||||
public void should_log_error_and_delete_if_musicbrainz_id_not_found_and_author_has_no_files()
|
||||
{
|
||||
GivenAlbumsForRefresh(new List<Album>());
|
||||
AllowArtistUpdate();
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Setup(x => x.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()));
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(new List<int> { _artist.Id }));
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.UpdateArtist(It.IsAny<Artist>(), It.IsAny<bool>()), Times.Never());
|
||||
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Metadata.Value.Status == ArtistStatusType.Deleted), true), Times.Once());
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Once());
|
||||
|
|
@ -197,16 +214,53 @@ public void should_log_error_but_not_delete_if_musicbrainz_id_not_found_and_arti
|
|||
{
|
||||
GivenArtistFiles();
|
||||
GivenAlbumsForRefresh(new List<Album>());
|
||||
AllowArtistUpdate();
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(new List<int> { _artist.Id }));
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Metadata.Value.Status == ArtistStatusType.Deleted), true), Times.Once());
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
ExceptionVerification.ExpectedErrors(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_log_error_if_musicbrainz_id_not_found()
|
||||
{
|
||||
GivenAlbumsForRefresh(new List<Album>());
|
||||
AllowArtistUpdate();
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Setup(x => x.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()));
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(new List<int> { _artist.Id }));
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Metadata.Value.Status == ArtistStatusType.Deleted), It.IsAny<bool>()), Times.Once());
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_remark_as_deleted_if_musicbrainz_id_not_found()
|
||||
{
|
||||
_artist.Metadata.Value.Status = ArtistStatusType.Deleted;
|
||||
GivenAlbumsForRefresh(new List<Album>());
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Setup(x => x.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()));
|
||||
|
||||
Subject.Execute(new RefreshArtistCommand(new List<int> { _artist.Id }));
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.UpdateArtist(It.IsAny<Artist>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<IArtistService>()
|
||||
.Verify(v => v.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
ExceptionVerification.ExpectedErrors(2);
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationType
|
||||
{
|
||||
None = 0,
|
||||
[Obsolete("Use Forms authentication instead")]
|
||||
Basic = 1,
|
||||
Forms = 2,
|
||||
External = 3
|
||||
|
|
|
|||
|
|
@ -204,13 +204,24 @@ public AuthenticationType AuthenticationMethod
|
|||
|
||||
if (enabled)
|
||||
{
|
||||
SetValue("AuthenticationMethod", AuthenticationType.Basic);
|
||||
return AuthenticationType.Basic;
|
||||
SetValue("AuthenticationMethod", AuthenticationType.Forms);
|
||||
return AuthenticationType.Forms;
|
||||
}
|
||||
|
||||
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
|
||||
var value = Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (value == AuthenticationType.Basic)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
SetValue("AuthenticationMethod", AuthenticationType.Forms);
|
||||
|
||||
return AuthenticationType.Forms;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,6 +396,12 @@ public void MigrateConfigFile()
|
|||
{
|
||||
SetValue("EnableSsl", false);
|
||||
}
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (AuthenticationMethod == AuthenticationType.Basic)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
SetValue("AuthenticationMethod", AuthenticationType.Forms);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteOldValues()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
|
|
@ -40,12 +45,31 @@ public interface IBasicRepository<TModel>
|
|||
public class BasicRepository<TModel> : IBasicRepository<TModel>
|
||||
where TModel : ModelBase, new()
|
||||
{
|
||||
private static readonly ILogger Logger = NzbDroneLogger.GetLogger(typeof(BasicRepository<TModel>));
|
||||
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly PropertyInfo _keyProperty;
|
||||
private readonly List<PropertyInfo> _properties;
|
||||
private readonly string _updateSql;
|
||||
private readonly string _insertSql;
|
||||
|
||||
private static ResiliencePipeline RetryStrategy => new ResiliencePipelineBuilder()
|
||||
.AddRetry(new RetryStrategyOptions
|
||||
{
|
||||
ShouldHandle = new PredicateBuilder().Handle<SQLiteException>(ex => ex.ResultCode == SQLiteErrorCode.Busy),
|
||||
Delay = TimeSpan.FromMilliseconds(100),
|
||||
MaxRetryAttempts = 3,
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
UseJitter = true,
|
||||
OnRetry = args =>
|
||||
{
|
||||
Logger.Warn(args.Outcome.Exception, "Failed writing to database. Retry #{0}", args.AttemptNumber);
|
||||
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
|
||||
protected readonly IDatabase _database;
|
||||
protected readonly string _table;
|
||||
|
||||
|
|
@ -186,7 +210,9 @@ private string GetInsertSql()
|
|||
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
|
||||
{
|
||||
SqlBuilderExtensions.LogQuery(_insertSql, model);
|
||||
var multi = connection.QueryMultiple(_insertSql, model, transaction);
|
||||
|
||||
var multi = RetryStrategy.Execute(static (state, _) => state.connection.QueryMultiple(state._insertSql, state.model, state.transaction), (connection, _insertSql, model, transaction));
|
||||
|
||||
var multiRead = multi.Read();
|
||||
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
||||
_keyProperty.SetValue(model, id);
|
||||
|
|
@ -381,7 +407,7 @@ private void UpdateFields(IDbConnection connection, IDbTransaction transaction,
|
|||
|
||||
SqlBuilderExtensions.LogQuery(sql, model);
|
||||
|
||||
connection.Execute(sql, model, transaction: transaction);
|
||||
RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.model, transaction: state.transaction), (connection, sql, model, transaction));
|
||||
}
|
||||
|
||||
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate)
|
||||
|
|
@ -393,7 +419,7 @@ private void UpdateFields(IDbConnection connection, IDbTransaction transaction,
|
|||
SqlBuilderExtensions.LogQuery(sql, model);
|
||||
}
|
||||
|
||||
connection.Execute(sql, models, transaction: transaction);
|
||||
RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.models, transaction: state.transaction), (connection, sql, models, transaction));
|
||||
}
|
||||
|
||||
protected virtual SqlBuilder PagedBuilder() => Builder();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
||||
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
|
||||
public class DatabaseEngineVersionCheck : ForwardOnlyMigration
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
|
|
@ -22,11 +22,6 @@ public override void Up()
|
|||
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
||||
}
|
||||
|
||||
public override void Down()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (var versionCmd = conn.CreateCommand())
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
using FluentMigrator.Runner.Initialization;
|
||||
using FluentMigrator.Runner.Processors;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NLog;
|
||||
using NLog.Extensions.Logging;
|
||||
|
||||
|
|
@ -20,13 +19,10 @@ public interface IMigrationController
|
|||
public class MigrationController : IMigrationController
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly ILoggerProvider _migrationLoggerProvider;
|
||||
|
||||
public MigrationController(Logger logger,
|
||||
ILoggerProvider migrationLoggerProvider)
|
||||
public MigrationController(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_migrationLoggerProvider = migrationLoggerProvider;
|
||||
}
|
||||
|
||||
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||
|
|
@ -35,16 +31,13 @@ public void Migrate(string connectionString, MigrationContext migrationContext,
|
|||
|
||||
_logger.Info("*** Migrating {0} ***", connectionString);
|
||||
|
||||
ServiceProvider serviceProvider;
|
||||
|
||||
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||
|
||||
serviceProvider = new ServiceCollection()
|
||||
var serviceProvider = new ServiceCollection()
|
||||
.AddLogging(b => b.AddNLog())
|
||||
.AddFluentMigratorCore()
|
||||
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
||||
.ConfigureRunner(
|
||||
builder => builder
|
||||
.ConfigureRunner(builder => builder
|
||||
.AddPostgres()
|
||||
.AddNzbDroneSQLite()
|
||||
.WithGlobalConnectionString(connectionString)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@
|
|||
using FluentMigrator.Builders.Create.Table;
|
||||
using FluentMigrator.Runner;
|
||||
using FluentMigrator.Runner.BatchParser;
|
||||
using FluentMigrator.Runner.Generators;
|
||||
using FluentMigrator.Runner.Generators.SQLite;
|
||||
using FluentMigrator.Runner.Initialization;
|
||||
using FluentMigrator.Runner.Processors;
|
||||
using FluentMigrator.Runner.Processors.SQLite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
{
|
||||
|
|
@ -26,23 +31,40 @@ public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction t
|
|||
return command;
|
||||
}
|
||||
|
||||
public static void AddParameter(this System.Data.IDbCommand command, object value)
|
||||
public static void AddParameter(this IDbCommand command, object value)
|
||||
{
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.Value = value;
|
||||
command.Parameters.Add(parameter);
|
||||
}
|
||||
|
||||
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder)
|
||||
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder, bool binaryGuid = false, bool useStrictTables = false)
|
||||
{
|
||||
builder.Services
|
||||
.AddTransient<SQLiteBatchParser>()
|
||||
.AddScoped<SQLiteDbFactory>()
|
||||
.AddScoped<NzbDroneSQLiteProcessor>()
|
||||
.AddScoped<NzbDroneSQLiteProcessor>(sp =>
|
||||
{
|
||||
var factory = sp.GetService<SQLiteDbFactory>();
|
||||
var logger = sp.GetService<ILogger<NzbDroneSQLiteProcessor>>();
|
||||
var options = sp.GetService<IOptionsSnapshot<ProcessorOptions>>();
|
||||
var connectionStringAccessor = sp.GetService<IConnectionStringAccessor>();
|
||||
var sqliteQuoter = new SQLiteQuoter(false);
|
||||
return new NzbDroneSQLiteProcessor(factory, sp.GetService<SQLiteGenerator>(), logger, options, connectionStringAccessor, sp, sqliteQuoter);
|
||||
})
|
||||
.AddScoped<ISQLiteTypeMap>(_ => new NzbDroneSQLiteTypeMap(useStrictTables))
|
||||
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
||||
.AddScoped<SQLiteQuoter>()
|
||||
.AddScoped<SQLiteGenerator>()
|
||||
.AddScoped(
|
||||
sp =>
|
||||
{
|
||||
var typeMap = sp.GetRequiredService<ISQLiteTypeMap>();
|
||||
return new SQLiteGenerator(
|
||||
new SQLiteQuoter(binaryGuid),
|
||||
typeMap,
|
||||
new OptionsWrapper<GeneratorOptions>(new GeneratorOptions()));
|
||||
})
|
||||
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,18 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||
{
|
||||
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
||||
{
|
||||
private readonly SQLiteQuoter _quoter;
|
||||
|
||||
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
||||
SQLiteGenerator generator,
|
||||
ILogger<NzbDroneSQLiteProcessor> logger,
|
||||
IOptionsSnapshot<ProcessorOptions> options,
|
||||
IConnectionStringAccessor connectionStringAccessor,
|
||||
IServiceProvider serviceProvider,
|
||||
SQLiteQuoter sqliteQuoter)
|
||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, sqliteQuoter)
|
||||
SQLiteQuoter quoter)
|
||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
||||
{
|
||||
_quoter = quoter;
|
||||
}
|
||||
|
||||
public override void Process(AlterColumnExpression expression)
|
||||
|
|
@ -35,7 +38,7 @@ public override void Process(AlterColumnExpression expression)
|
|||
|
||||
if (columnIndex == -1)
|
||||
{
|
||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.Column.Name, expression.TableName));
|
||||
throw new ApplicationException($"Column {expression.Column.Name} does not exist on table {expression.TableName}.");
|
||||
}
|
||||
|
||||
columnDefinitions[columnIndex] = expression.Column;
|
||||
|
|
@ -45,6 +48,28 @@ public override void Process(AlterColumnExpression expression)
|
|||
ProcessAlterTable(tableDefinition);
|
||||
}
|
||||
|
||||
public override void Process(AlterDefaultConstraintExpression expression)
|
||||
{
|
||||
var tableDefinition = GetTableSchema(expression.TableName);
|
||||
|
||||
var columnDefinitions = tableDefinition.Columns.ToList();
|
||||
var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.ColumnName);
|
||||
|
||||
if (columnIndex == -1)
|
||||
{
|
||||
throw new ApplicationException($"Column {expression.ColumnName} does not exist on table {expression.TableName}.");
|
||||
}
|
||||
|
||||
var changedColumn = columnDefinitions[columnIndex];
|
||||
changedColumn.DefaultValue = expression.DefaultValue;
|
||||
|
||||
columnDefinitions[columnIndex] = changedColumn;
|
||||
|
||||
tableDefinition.Columns = columnDefinitions;
|
||||
|
||||
ProcessAlterTable(tableDefinition);
|
||||
}
|
||||
|
||||
public override void Process(DeleteColumnExpression expression)
|
||||
{
|
||||
var tableDefinition = GetTableSchema(expression.TableName);
|
||||
|
|
@ -62,7 +87,7 @@ public override void Process(DeleteColumnExpression expression)
|
|||
|
||||
if (columnsToRemove.Any())
|
||||
{
|
||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", columnsToRemove.First(), expression.TableName));
|
||||
throw new ApplicationException($"Column {columnsToRemove.First()} does not exist on table {expression.TableName}.");
|
||||
}
|
||||
|
||||
ProcessAlterTable(tableDefinition);
|
||||
|
|
@ -78,12 +103,12 @@ public override void Process(RenameColumnExpression expression)
|
|||
|
||||
if (columnIndex == -1)
|
||||
{
|
||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.OldName, expression.TableName));
|
||||
throw new ApplicationException($"Column {expression.OldName} does not exist on table {expression.TableName}.");
|
||||
}
|
||||
|
||||
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
||||
{
|
||||
throw new ApplicationException(string.Format("Column {0} already exists on table {1}.", expression.NewName, expression.TableName));
|
||||
throw new ApplicationException($"Column {expression.NewName} already exists on table {expression.TableName}.");
|
||||
}
|
||||
|
||||
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
||||
|
|
@ -128,21 +153,20 @@ protected virtual void ProcessAlterTable(TableDefinition tableDefinition, List<C
|
|||
}
|
||||
|
||||
// What is the cleanest way to do this? Add function to Generator?
|
||||
var quoter = new SQLiteQuoter();
|
||||
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name)));
|
||||
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => quoter.QuoteColumnName(c.Name)));
|
||||
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => _quoter.QuoteColumnName(c.Name)));
|
||||
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => _quoter.QuoteColumnName(c.Name)));
|
||||
|
||||
Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
||||
Process(new CreateTableExpression { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
||||
|
||||
Process(string.Format("INSERT INTO {0} ({1}) SELECT {2} FROM {3}", quoter.QuoteTableName(tempTableName), columnsToInsert, columnsToFetch, quoter.QuoteTableName(tableName)));
|
||||
Process($"INSERT INTO {_quoter.QuoteTableName(tempTableName)} ({columnsToInsert}) SELECT {columnsToFetch} FROM {_quoter.QuoteTableName(tableName)}");
|
||||
|
||||
Process(new DeleteTableExpression() { TableName = tableName });
|
||||
Process(new DeleteTableExpression { TableName = tableName });
|
||||
|
||||
Process(new RenameTableExpression() { OldName = tempTableName, NewName = tableName });
|
||||
Process(new RenameTableExpression { OldName = tempTableName, NewName = tableName });
|
||||
|
||||
foreach (var index in tableDefinition.Indexes)
|
||||
{
|
||||
Process(new CreateIndexExpression() { Index = index });
|
||||
Process(new CreateIndexExpression { Index = index });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
using System.Data;
|
||||
using FluentMigrator.Runner.Generators.Base;
|
||||
using FluentMigrator.Runner.Generators.SQLite;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
// Based on https://github.com/fluentmigrator/fluentmigrator/blob/v6.2.0/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteTypeMap.cs
|
||||
public sealed class NzbDroneSQLiteTypeMap : TypeMapBase, ISQLiteTypeMap
|
||||
{
|
||||
public bool UseStrictTables { get; }
|
||||
|
||||
public NzbDroneSQLiteTypeMap(bool useStrictTables = false)
|
||||
{
|
||||
UseStrictTables = useStrictTables;
|
||||
|
||||
SetupTypeMaps();
|
||||
}
|
||||
|
||||
// Must be kept in sync with upstream
|
||||
protected override void SetupTypeMaps()
|
||||
{
|
||||
SetTypeMap(DbType.Binary, "BLOB");
|
||||
SetTypeMap(DbType.Byte, "INTEGER");
|
||||
SetTypeMap(DbType.Int16, "INTEGER");
|
||||
SetTypeMap(DbType.Int32, "INTEGER");
|
||||
SetTypeMap(DbType.Int64, "INTEGER");
|
||||
SetTypeMap(DbType.SByte, "INTEGER");
|
||||
SetTypeMap(DbType.UInt16, "INTEGER");
|
||||
SetTypeMap(DbType.UInt32, "INTEGER");
|
||||
SetTypeMap(DbType.UInt64, "INTEGER");
|
||||
|
||||
if (!UseStrictTables)
|
||||
{
|
||||
SetTypeMap(DbType.Currency, "NUMERIC");
|
||||
SetTypeMap(DbType.Decimal, "NUMERIC");
|
||||
SetTypeMap(DbType.Double, "NUMERIC");
|
||||
SetTypeMap(DbType.Single, "NUMERIC");
|
||||
SetTypeMap(DbType.VarNumeric, "NUMERIC");
|
||||
SetTypeMap(DbType.Date, "DATETIME");
|
||||
SetTypeMap(DbType.DateTime, "DATETIME");
|
||||
SetTypeMap(DbType.DateTime2, "DATETIME");
|
||||
SetTypeMap(DbType.Time, "DATETIME");
|
||||
SetTypeMap(DbType.Guid, "UNIQUEIDENTIFIER");
|
||||
|
||||
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
||||
SetTypeMap(DbType.DateTimeOffset, "DATETIME");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetTypeMap(DbType.Currency, "TEXT");
|
||||
SetTypeMap(DbType.Decimal, "TEXT");
|
||||
SetTypeMap(DbType.Double, "REAL");
|
||||
SetTypeMap(DbType.Single, "REAL");
|
||||
SetTypeMap(DbType.VarNumeric, "TEXT");
|
||||
SetTypeMap(DbType.Date, "TEXT");
|
||||
SetTypeMap(DbType.DateTime, "TEXT");
|
||||
SetTypeMap(DbType.DateTime2, "TEXT");
|
||||
SetTypeMap(DbType.Time, "TEXT");
|
||||
SetTypeMap(DbType.Guid, "TEXT");
|
||||
|
||||
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
||||
SetTypeMap(DbType.DateTimeOffset, "TEXT");
|
||||
}
|
||||
|
||||
SetTypeMap(DbType.AnsiString, "TEXT");
|
||||
SetTypeMap(DbType.String, "TEXT");
|
||||
SetTypeMap(DbType.AnsiStringFixedLength, "TEXT");
|
||||
SetTypeMap(DbType.StringFixedLength, "TEXT");
|
||||
SetTypeMap(DbType.Boolean, "INTEGER");
|
||||
}
|
||||
|
||||
public override string GetTypeMap(DbType type, int? size, int? precision)
|
||||
{
|
||||
return base.GetTypeMap(type, size: null, precision: null);
|
||||
}
|
||||
}
|
||||
|
|
@ -424,8 +424,8 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSe
|
|||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_logger.Debug("qbitTorrent authentication failed.");
|
||||
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
_logger.Debug(ex, "qbitTorrent authentication failed.");
|
||||
if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
|
||||
}
|
||||
|
|
@ -438,7 +438,7 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSe
|
|||
}
|
||||
|
||||
// returns "Fails." on bad login
|
||||
if (response.Content != "Ok.")
|
||||
if (response.Content.IsNotNullOrWhiteSpace() && response.Content != "Ok.")
|
||||
{
|
||||
_logger.Debug("qbitTorrent authentication failed.");
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ private string DownloadFromMagnetUrl(RemoteAlbum remoteAlbum, IIndexer indexer,
|
|||
|
||||
try
|
||||
{
|
||||
hash = MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
|
||||
hash = MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -111,24 +111,26 @@ private List<TrackedDownload> ProcessClientDownloads(IDownloadClient downloadCli
|
|||
|
||||
private TrackedDownload ProcessClientItem(IDownloadClient downloadClient, DownloadClientItem downloadItem)
|
||||
{
|
||||
TrackedDownload trackedDownload = null;
|
||||
|
||||
try
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem);
|
||||
trackedDownload =
|
||||
_trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition,
|
||||
downloadItem);
|
||||
|
||||
if (trackedDownload is { State: TrackedDownloadState.Downloading or TrackedDownloadState.ImportBlocked })
|
||||
{
|
||||
_failedDownloadService.Check(trackedDownload);
|
||||
_completedDownloadService.Check(trackedDownload);
|
||||
}
|
||||
|
||||
return trackedDownload;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Couldn't process tracked download {0}", downloadItem.Title);
|
||||
}
|
||||
|
||||
return null;
|
||||
return trackedDownload;
|
||||
}
|
||||
|
||||
private bool DownloadIsTrackable(TrackedDownload trackedDownload)
|
||||
|
|
|
|||
|
|
@ -169,8 +169,8 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
|||
{
|
||||
trackedDownload.RemoteAlbum = _parsingService.Map(parsedAlbumInfo,
|
||||
firstHistoryItem.ArtistId,
|
||||
historyItems.Where(v => v.EventType == EntityHistoryEventType.Grabbed).Select(h => h.AlbumId)
|
||||
.Distinct());
|
||||
historyItems.Where(v => v.EventType == EntityHistoryEventType.Grabbed)
|
||||
.Select(h => h.AlbumId).Distinct());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -183,8 +183,8 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
|||
{
|
||||
trackedDownload.RemoteAlbum = _parsingService.Map(parsedAlbumInfo,
|
||||
firstHistoryItem.ArtistId,
|
||||
historyItems.Where(v => v.EventType == EntityHistoryEventType.Grabbed).Select(h => h.AlbumId)
|
||||
.Distinct());
|
||||
historyItems.Where(v => v.EventType == EntityHistoryEventType.Grabbed)
|
||||
.Select(h => h.AlbumId).Distinct());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -211,10 +211,17 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
|||
_logger.Trace("No Album found for download '{0}'", trackedDownload.DownloadItem.Title);
|
||||
}
|
||||
}
|
||||
catch (MultipleArtistsFoundException e)
|
||||
{
|
||||
_logger.Debug(e, "Found multiple artists for " + downloadItem.Title);
|
||||
|
||||
trackedDownload.Warn("Unable to import automatically, found multiple artists: {0}", string.Join(", ", e.Artists));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Failed to find album for " + downloadItem.Title);
|
||||
return null;
|
||||
|
||||
trackedDownload.Warn("Unable to parse albums from title");
|
||||
}
|
||||
|
||||
LogItemChange(trackedDownload, existingItem?.DownloadItem, trackedDownload.DownloadItem);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
|||
{
|
||||
[CheckOn(typeof(ArtistUpdatedEvent))]
|
||||
[CheckOn(typeof(ArtistsDeletedEvent), CheckOnCondition.FailedOnly)]
|
||||
[CheckOn(typeof(ArtistRefreshCompleteEvent))]
|
||||
public class RemovedArtistCheck : HealthCheckBase, ICheckOnCondition<ArtistUpdatedEvent>, ICheckOnCondition<ArtistsDeletedEvent>
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ public List<ImportListItemInfo> Fetch()
|
|||
|
||||
Task.WaitAll(taskList.ToArray());
|
||||
|
||||
result = result.DistinctBy(r => new { r.Artist, r.Album, r.ArtistMusicBrainzId }).ToList();
|
||||
result = result.DistinctBy(r => new { r.Artist, r.Album, r.ArtistMusicBrainzId, r.AlbumMusicBrainzId }).ToList();
|
||||
|
||||
_logger.Debug("Found {0} total reports from {1} lists", result.Count, importLists.Count);
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ public List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition)
|
|||
|
||||
Task.WaitAll(taskList.ToArray());
|
||||
|
||||
result = result.DistinctBy(r => new { r.Artist, r.Album, r.ArtistMusicBrainzId }).ToList();
|
||||
result = result.DistinctBy(r => new { r.Artist, r.Album, r.ArtistMusicBrainzId, r.AlbumMusicBrainzId }).ToList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,12 +166,10 @@ protected virtual IList<ImportListItemInfo> FetchReleases(Func<IImportListReques
|
|||
|
||||
protected virtual bool IsValidRelease(ImportListItemInfo release)
|
||||
{
|
||||
if (release.Album.IsNullOrWhiteSpace() && release.Artist.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return release.Album.IsNotNullOrWhiteSpace()
|
||||
|| release.Artist.IsNotNullOrWhiteSpace()
|
||||
|| release.AlbumMusicBrainzId.IsNotNullOrWhiteSpace()
|
||||
|| release.ArtistMusicBrainzId.IsNotNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
protected virtual bool IsFullPage(IList<ImportListItemInfo> page)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ protected virtual string GetInfoHash(XElement item)
|
|||
{
|
||||
try
|
||||
{
|
||||
return MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
|
||||
return MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,34 +1,35 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.151" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
||||
<PackageReference Include="Equ" Version="2.3.0" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="Polly" Version="8.5.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageReference Include="System.Memory" Version="4.6.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageReference Include="MailKit" Version="4.14.0" />
|
||||
<PackageReference Include="Polly" Version="8.6.4" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.6" />
|
||||
<PackageReference Include="System.Memory" Version="4.6.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.20" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Core" Version="6.2.0" />
|
||||
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="6.2.0" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="6.2.0" />
|
||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="NLog" Version="5.4.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.4.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.27" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.10" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="SpotifyAPI.Web" Version="5.1.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||
<PackageReference Include="MonoTorrent" Version="3.0.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.20" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Common\Lidarr.Common.csproj" />
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@
|
|||
"ArtistNameHelpText": "The name of the artist/album to exclude (can be anything meaningful)",
|
||||
"ArtistProgressBarText": "{trackFileCount} / {trackCount} (Total: {totalTrackCount}, Downloading: {downloadingCount})",
|
||||
"ArtistType": "Artist Type",
|
||||
"ArtistWasDeletedFromMusicBrainz": "Artist was deleted from MusicBrainz",
|
||||
"Artists": "Artists",
|
||||
"ArtistsEditRootFolderHelpText": "Moving artists to the same root folder can be used to rename artist folders to match updated name or naming format",
|
||||
"AudioInfo": "Audio Info",
|
||||
|
|
@ -498,6 +499,7 @@
|
|||
"Files": "Files",
|
||||
"FilterAlbumPlaceholder": "Filter album",
|
||||
"FilterArtistPlaceholder": "Filter artist",
|
||||
"FilterTracksByTitleOrNumber": "Filter tracks by title or number",
|
||||
"Filters": "Filters",
|
||||
"FirstAlbum": "First Album",
|
||||
"FirstAlbumData": "Monitor the first albums. All other albums will be ignored",
|
||||
|
|
@ -828,6 +830,7 @@
|
|||
"NoMediumInformation": "No medium information is available.",
|
||||
"NoMinimumForAnyDuration": "No minimum for any duration",
|
||||
"NoMissingItems": "No missing items",
|
||||
"NoTracksFoundForSelectedAlbum": "No tracks were found for the selected album",
|
||||
"NoResultsFound": "No results found",
|
||||
"NoTagsHaveBeenAddedYet": "No tags have been added yet",
|
||||
"NoTracksInThisMedium": "No tracks in this medium",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"AlbumStudio": "Studio Album",
|
||||
"AllAlbums": "Tutti gli album",
|
||||
"Actions": "Azioni",
|
||||
"AllAlbumsData": "Monitora tutti gli album tranne quelli speciali",
|
||||
"AllAlbumsData": "Monitora tutti gli album",
|
||||
"AllowFingerprintingHelpText": "Consenti impronte per migliorare il riconoscimento delle tracce",
|
||||
"AllowFingerprintingHelpTextWarning": "Questo richiede che {appName} legga le parti dei file e potrebbe causare un’alta attività del disco e della rete.",
|
||||
"AllArtistAlbums": "Tutti gli Artisti degli album",
|
||||
|
|
@ -95,17 +95,17 @@
|
|||
"DeleteImportListMessageText": "Sei sicuro di voler eliminare la lista '{name}'?",
|
||||
"DeleteIndexer": "Cancella Indicizzatore",
|
||||
"DeleteIndexerMessageText": "Sei sicuro di voler eliminare l'indicizzatore '{name}'?",
|
||||
"DeleteMetadataProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {0}",
|
||||
"DeleteMetadataProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {name}",
|
||||
"DeleteNotification": "Cancella Notifica",
|
||||
"DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{name}'?",
|
||||
"DeleteQualityProfile": "Cancellare il profilo di qualità",
|
||||
"DeleteQualityProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {0}",
|
||||
"DeleteReleaseProfile": "Cancellare il profilo di ritardo",
|
||||
"DeleteReleaseProfileMessageText": "Sei sicuro di voler cancellare questo profilo di ritardo?",
|
||||
"DeleteQualityProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {name}?",
|
||||
"DeleteReleaseProfile": "Cancellare il profilo di Rilascio",
|
||||
"DeleteReleaseProfileMessageText": "Sei sicuro di voler cancellare questo profilo di rilascio?",
|
||||
"DeleteSelectedTrackFiles": "Cancellare i film selezionati",
|
||||
"DeleteSelectedTrackFilesMessageText": "Sei sicuro di voler eliminare i file del film selezionato?",
|
||||
"DeleteTag": "Cancella Tag",
|
||||
"DeleteTagMessageText": "Sei sicuro di voler eliminare il tag '{0}'?",
|
||||
"DeleteTagMessageText": "Sei sicuro di voler eliminare il tag '{label}'?",
|
||||
"DestinationPath": "Percorso di destinazione",
|
||||
"DetailedProgressBar": "Barra di avanzamento dettagliata",
|
||||
"DetailedProgressBarHelpText": "Mostra testo sulla barra di avanzamento",
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
"Fixed": "Fissato",
|
||||
"Folder": "Cartella",
|
||||
"Folders": "Cartelle",
|
||||
"ForMoreInformationOnTheIndividualDownloadClientsClickOnTheInfoButtons": "Per maggiori informazioni sui singoli Indexer clicca sul pulsante info.",
|
||||
"ForMoreInformationOnTheIndividualDownloadClientsClickOnTheInfoButtons": "Per maggiori informazioni sui singoli client di download, clicca sul pulsante info.",
|
||||
"ForMoreInformationOnTheIndividualIndexersClickOnTheInfoButtons": "Per maggiori informazioni sui singoli Indexer clicca sul pulsante info.",
|
||||
"ForMoreInformationOnTheIndividualListsClickOnTheInfoButtons": "Per maggiori informazioni sui singoli Indexer clicca sul pulsante info.",
|
||||
"GeneralSettings": "Impostazioni Generali",
|
||||
|
|
@ -198,7 +198,7 @@
|
|||
"LogLevelvalueTraceTraceLoggingShouldOnlyBeEnabledTemporarily": "Il Trace Log dovrebbe essere abilitato solo temporaneamente",
|
||||
"Logs": "Logs",
|
||||
"LongDateFormat": "Formato Data Lungo",
|
||||
"MaintenanceRelease": "Release di Manutenzione",
|
||||
"MaintenanceRelease": "Release di Manutenzione: bug fix e altri miglioramenti. Vedi la storia dei commit Github per maggiori dettagli",
|
||||
"ManualImport": "Import Manuale",
|
||||
"MarkAsFailed": "Segna come fallito",
|
||||
"MarkAsFailedMessageText": "Sei sicuro di voler segnare '{0}' come fallito?",
|
||||
|
|
@ -664,7 +664,7 @@
|
|||
"CutoffFormatScoreHelpText": "Una volta raggiunto questo formato personalizzato, {appName} non scaricherà più i film",
|
||||
"DeleteCustomFormat": "Cancella Formato Personalizzato",
|
||||
"DeleteCustomFormatMessageText": "Sei sicuro di voler eliminare il formato personalizzato '{name}'?",
|
||||
"DeleteFormatMessageText": "Sei sicuro di voler cancellare il formato etichetta {0} ?",
|
||||
"DeleteFormatMessageText": "Sei sicuro di voler cancellare il formato etichetta {name} ?",
|
||||
"DownloadPropersAndRepacksHelpTextWarning": "Usa i formati personalizzati per aggiornare automaticamente ai Proper/Repack",
|
||||
"DownloadedUnableToImportCheckLogsForDetails": "Scaricato - Impossibile importare: controlla i log per i dettagli",
|
||||
"ExportCustomFormat": "Esporta formato personalizzato",
|
||||
|
|
@ -732,7 +732,7 @@
|
|||
"BlocklistReleaseHelpText": "Impedisci a {appName} di re-acquisire automaticamente questa versione",
|
||||
"FailedToLoadQueue": "Impossibile caricare la coda",
|
||||
"BlocklistReleases": "Blocca questa Release",
|
||||
"DeleteConditionMessageText": "Sei sicuro di voler eliminare l'etichetta '{0}'?",
|
||||
"DeleteConditionMessageText": "Sei sicuro di voler eliminare l'etichetta '{name}'?",
|
||||
"Negated": "Negato",
|
||||
"RemoveSelectedItems": "Rimuovi elementi selezionati",
|
||||
"Required": "necessario",
|
||||
|
|
@ -755,9 +755,9 @@
|
|||
"Yes": "Si",
|
||||
"RemoveSelectedItemQueueMessageText": "Sei sicuro di voler rimuovere {0} dalla coda?",
|
||||
"RemoveSelectedItemsQueueMessageText": "Sei sicuro di voler rimuovere {0} dalla coda?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?",
|
||||
"DeleteSelectedImportListsMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?",
|
||||
"DeleteSelectedIndexersMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare {count} client di download selezionato(i)?",
|
||||
"DeleteSelectedImportListsMessageText": "Sei sicuro di voler eliminare {count} lista(e) di importazione selezionata(e)?",
|
||||
"DeleteSelectedIndexersMessageText": "Sei sicuro di voler eliminare {count} indice(i) selezionato(i)?",
|
||||
"ApplyTagsHelpTextHowToApplyArtists": "Come applicare etichette agli indicizzatori selezionati",
|
||||
"ApplyTagsHelpTextHowToApplyDownloadClients": "Come applicare etichette ai client di download selezionati",
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "Come applicare etichette alle liste di importazione selezionate",
|
||||
|
|
|
|||
|
|
@ -126,11 +126,11 @@ public bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Artist artist)
|
|||
|
||||
if (albumParseResult == null)
|
||||
{
|
||||
_logger.Warn("Unable to parse file on import: [{0}]", audioFile);
|
||||
_logger.Warn("Unable to parse file on import: [{0}]", audioFile.FullName);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Warn("Audio file detected: [{0}]", audioFile);
|
||||
_logger.Warn("Audio file detected: [{0}]", audioFile.FullName);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public string GetHashFromTorrentFile(byte[] fileContents)
|
|||
{
|
||||
try
|
||||
{
|
||||
return Torrent.Load(fileContents).InfoHash.ToHex();
|
||||
return Torrent.Load(fileContents).InfoHashes.V1OrV2.ToHex();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -155,9 +155,9 @@ private bool FingerprintingAllowed(bool newDownload)
|
|||
private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease)
|
||||
{
|
||||
var worstTrackMatchDist = localAlbumRelease.TrackMapping?.Mapping
|
||||
.DefaultIfEmpty()
|
||||
.MaxBy(x => x.Value.Item2.NormalizedDistance())
|
||||
.Value.Item2.NormalizedDistance() ?? 1.0;
|
||||
.Select(x => x.Value.Item2.NormalizedDistance())
|
||||
.DefaultIfEmpty(1.0)
|
||||
.Max() ?? 1.0;
|
||||
|
||||
if (localAlbumRelease.Distance.NormalizedDistance() > 0.15 ||
|
||||
localAlbumRelease.TrackMapping.LocalExtra.Any() ||
|
||||
|
|
|
|||
|
|
@ -153,7 +153,15 @@ private List<ManualImportItem> ProcessFolder(string folder, string downloadId, A
|
|||
{
|
||||
DownloadClientItem downloadClientItem = null;
|
||||
var directoryInfo = new DirectoryInfo(folder);
|
||||
artist = artist ?? _parsingService.GetArtist(directoryInfo.Name);
|
||||
|
||||
try
|
||||
{
|
||||
artist ??= _parsingService.GetArtist(directoryInfo.Name);
|
||||
}
|
||||
catch (MultipleArtistsFoundException e)
|
||||
{
|
||||
_logger.Warn(e, "Unable to find artist from title");
|
||||
}
|
||||
|
||||
if (downloadId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||
{
|
||||
|
|
@ -19,10 +18,5 @@ public RecycleBinException(string message, Exception innerException)
|
|||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected RecycleBinException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||
{
|
||||
|
|
@ -19,10 +18,5 @@ public RootFolderNotFoundException(string message, Exception innerException)
|
|||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected RootFolderNotFoundException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/NzbDrone.Core/Music/MultipleArtistsFoundException.cs
Normal file
16
src/NzbDrone.Core/Music/MultipleArtistsFoundException.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class MultipleArtistsFoundException : NzbDroneException
|
||||
{
|
||||
public List<Artist> Artists { get; }
|
||||
|
||||
public MultipleArtistsFoundException(List<Artist> artists, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
Artists = artists;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,9 @@ public Artist FindByName(string cleanName)
|
|||
{
|
||||
cleanName = cleanName.ToLowerInvariant();
|
||||
|
||||
return Query(s => s.CleanName == cleanName).ExclusiveOrDefault();
|
||||
var artists = Query(s => s.CleanName == cleanName).ToList();
|
||||
|
||||
return ReturnSingleArtistOrThrow(artists);
|
||||
}
|
||||
|
||||
public Artist GetArtistByMetadataId(int artistMetadataId)
|
||||
|
|
@ -91,5 +93,20 @@ public List<Artist> GetArtistByMetadataId(IEnumerable<int> artistMetadataIds)
|
|||
{
|
||||
return Query(s => artistMetadataIds.Contains(s.ArtistMetadataId));
|
||||
}
|
||||
|
||||
private static Artist ReturnSingleArtistOrThrow(List<Artist> artists)
|
||||
{
|
||||
if (artists.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (artists.Count == 1)
|
||||
{
|
||||
return artists[0];
|
||||
}
|
||||
|
||||
throw new MultipleArtistsFoundException(artists, "Expected one artist, but found {0}. Matching artists: {1}", artists.Count, string.Join(", ", artists));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,19 +337,23 @@ private void RescanArtists(List<Artist> artists, bool isNew, CommandTrigger trig
|
|||
private void RefreshSelectedArtists(List<int> artistIds, bool isNew, CommandTrigger trigger)
|
||||
{
|
||||
var updated = false;
|
||||
var artists = _artistService.GetArtists(artistIds);
|
||||
var artists = new List<Artist>();
|
||||
|
||||
foreach (var artist in artists)
|
||||
foreach (var artistId in artistIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var artist = _artistService.GetArtist(artistId);
|
||||
updated |= RefreshEntityInfo(artist, null, true, false, null);
|
||||
UpdateTags(artist);
|
||||
artists.Add(artist);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var artist = _artistService.GetArtist(artistId);
|
||||
_logger.Error(e, "Couldn't refresh info for Artist '{0}' [{1}]", artist.Name, artist.ForeignArtistId);
|
||||
UpdateTags(artist);
|
||||
artists.Add(artist);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -418,27 +422,29 @@ public void Execute(RefreshArtistCommand message)
|
|||
|
||||
foreach (var artist in artists)
|
||||
{
|
||||
var artistLocal = artist;
|
||||
var manualTrigger = message.Trigger == CommandTrigger.Manual;
|
||||
|
||||
if ((updatedMusicbrainzArtists == null && _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist)) ||
|
||||
(updatedMusicbrainzArtists != null && updatedMusicbrainzArtists.Contains(artist.ForeignArtistId)) ||
|
||||
if ((updatedMusicbrainzArtists == null && _checkIfArtistShouldBeRefreshed.ShouldRefresh(artistLocal)) ||
|
||||
(updatedMusicbrainzArtists != null && updatedMusicbrainzArtists.Contains(artistLocal.ForeignArtistId)) ||
|
||||
manualTrigger)
|
||||
{
|
||||
try
|
||||
{
|
||||
updated |= RefreshEntityInfo(artist, null, manualTrigger, false, message.LastStartTime);
|
||||
artistLocal = _artistService.GetArtist(artistLocal.Id);
|
||||
updated |= RefreshEntityInfo(artistLocal, null, manualTrigger, false, message.LastStartTime);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Couldn't refresh info for Artist '{0}' [{1}]", artist.Name, artist.ForeignArtistId);
|
||||
_logger.Error(e, "Couldn't refresh info for Artist '{0}' [{1}]", artistLocal.Name, artistLocal.ForeignArtistId);
|
||||
}
|
||||
|
||||
UpdateTags(artist);
|
||||
UpdateTags(artistLocal);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("Skipping refresh of Artist '{0}' [{1}]", artist.Name, artist.ForeignArtistId);
|
||||
UpdateTags(artist);
|
||||
_logger.Info("Skipping refresh of Artist '{0}' [{1}]", artistLocal.Name, artistLocal.ForeignArtistId);
|
||||
UpdateTags(artistLocal);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Host\Lidarr.Host.csproj" />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.6" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue